Índice

  1. Introducción
  2. Cargar librerías
  3. Cargar datos
  4. Visualización y preprocesamiento
    4.1 Imputación de valores nulos
    4.2 Codificación de variables categóricas
    4.3 Escalado de variables numéricas
  5. Análisis de datos
    5.1 Análisis aprendizaje supervisado
    5.2 Análisis aprendizaje no supervisado
    5.2.1 K-means
    5.2.2 Clustering jerárquico

Introducción

Este trabajo se centra en el análisis de un conjunto de datos que contiene información sobre 61069 champiñones diferentes. Los datos han sido obtenidos de Kaggle y cada una de las instancias incluye 21 variables que describen diferentes aspectos de los champiñones, como su forma, tamaño y hábitat. En estas variables se encuentra inlcuida la clase de cada champiñón, indicando si es venenoso o comestible.

Antes de comenzar el análisis, es necesario realizar una fase de preprocesamiento de los datos. Esta fase tiene como objetivo preparar los datos para su análisis y obtener mayor conocimiento sobre estos. Durante esta fase, realizaremos tareas como visualizar los datos, detectar y tratar valores faltantes, o normalizar los datos. Una vez preparados los datos, procederemos a realizar el análisis. Para ello, utilizaremos tanto técnicas de aprendizaje supervisado como no supervisado.

El aprendizaje supervisado implica entrenar un modelo con datos etiquetados, es decir, que ya conocemos la clase de cada instancia. Una vez entrenado el modelo, podemos utilizarlo para predecir la clase de nuevas instancias. En este caso, utilizaremos diferentes algoritmos de clasificación para evaluar diferentes modelos.

El aprendizaje no supervisado, por otro lado, no requiere de datos etiquetados. En este caso, el objetivo es agrupar las instancias en diferentes grupos de forma que las instancias de un mismo grupo sean similares entre sí y diferentes a las de los demás grupos. Utilizaremos dos algoritmos de clustering para evaluar diferentes modelos. Además, cabe destacar, que en nuestro caso contamos con un conocimiento a priori de los datos, ya que conocemos la clase de cada instancia. Por tanto, podemos utilizar este conocimiento para evaluar los modelos de clustering.

A continuación se detalla los posibles valores que pueden tomar las variables del dataset:

1. class: edible=e, poisonous=p
2. cap-diameter: float number in cm
3. cap-shape: bell=b, conical=c, convex=x, flat=f, knobbed=k, sunken=s
4. cap-surface: fibrous=f, grooves=g, scaly=y, smooth=s
5. cap-color: brown=n, buff=b, cinnamon=c, gray=g, green=r, pink=p, purple=u, red=e, white=w, yellow=y
6. does.bruise.or.bleed: bruises-or-bleeding=t,no=f
7. gill-attachment: attached=a, descending=d, free=f, notched=n
8. gill-spacing: close=c, crowded=w, distant=d
9. gill-color: black=k, brown=n, buff=b, chocolate=h, gray=g, green=r, orange=o, pink=p, purple=u, red=e, white=w, yellow=y
10. stem-height: float number in cm
11. stem-width: float number in mm
12. stem-root: bulbous=b, swollen=s, club=c, cup=u, equal=e, rhizomorphs=z, rooted=r
13. stem-surface: see cap-surface + none=f
14. stem-color: see cap-color + none=f
15. veil-type: partial=p, universal=u
16. veil-color: see cap-color + none=f
17. has-ring: ring=t, none=f
18. ring-type: cobwebby=c, evanescent=e, flaring=r, grooved=g, large=l, pendant=p, sheathing=s, zone=z, scaly=y, movable=m, none=f, unknown=?
19. spore-print-color: see cap color
20. habitat: grasses=g, leaves=l, meadows=m, paths=p, heaths=h, urban=u, waste=w, woods=d
21. season: spring=s, summer=u, autumn=a, winter=w

Autores

Este trabajo ha sido realizado por:

Todos los integrantes del grupo han realizado de forma conjunta la visualización y el preprocesamiento de los datos. Ana y Deyan se ha encargado de realizar el aprendizaje supervisado, mientras que Javier y María Isabel se han encargado de realizar el aprendizaje no supervisado. Por otro lado, todos los componentes han investigado acerca del clustering utilizado en BigML.

Cargar librerías

En los siguientes fragmentos se incluye el código necesario para instalar y cargar las librerías necesarias para el análisis de los datos realizado.

#install.packages("caret")
#install.packages("tidyverse")
#install.packages("plotly")
#install.packages("dplyr")
#install.packages("factoextra")
#install.packages("dendextend")
library(caret)
library(tidyverse)
library(plotly)
library(dplyr)
library(cluster)
library(factoextra)
library(dendextend)

Cargar datos

En primer lugar, realizamos la lectura del fichero csv, separando las columnas por “;” y mostramos las primeras 6 filas del fichero. Este código nos permitirá acceder a los datos de champiñones y trabajar con ellos en nuestro código R.

mushroom <- read.csv("./data/data.csv", sep = ";")
head(mushroom)

Echamos un vistazo a las características de los atributos del dataset. En el caso de las variables numéricas, se puede observar valores como el mínimo, máximo, media, desviación estándar, etc. Por otro lado, en el caso de las variables categóricas no obtenemos información relevante.

summary(mushroom)
    class            cap.diameter     cap.shape         cap.surface         cap.color         does.bruise.or.bleed
 Length:61069       Min.   : 0.380   Length:61069       Length:61069       Length:61069       Length:61069        
 Class :character   1st Qu.: 3.480   Class :character   Class :character   Class :character   Class :character    
 Mode  :character   Median : 5.860   Mode  :character   Mode  :character   Mode  :character   Mode  :character    
                    Mean   : 6.734                                                                                
                    3rd Qu.: 8.540                                                                                
                    Max.   :62.340                                                                                
 gill.attachment    gill.spacing        gill.color         stem.height       stem.width      stem.root        
 Length:61069       Length:61069       Length:61069       Min.   : 0.000   Min.   :  0.00   Length:61069      
 Class :character   Class :character   Class :character   1st Qu.: 4.640   1st Qu.:  5.21   Class :character  
 Mode  :character   Mode  :character   Mode  :character   Median : 5.950   Median : 10.19   Mode  :character  
                                                          Mean   : 6.582   Mean   : 12.15                     
                                                          3rd Qu.: 7.740   3rd Qu.: 16.57                     
                                                          Max.   :33.920   Max.   :103.91                     
 stem.surface        stem.color         veil.type          veil.color          has.ring          ring.type        
 Length:61069       Length:61069       Length:61069       Length:61069       Length:61069       Length:61069      
 Class :character   Class :character   Class :character   Class :character   Class :character   Class :character  
 Mode  :character   Mode  :character   Mode  :character   Mode  :character   Mode  :character   Mode  :character  
                                                                                                                  
                                                                                                                  
                                                                                                                  
 spore.print.color    habitat             season         
 Length:61069       Length:61069       Length:61069      
 Class :character   Class :character   Class :character  
 Mode  :character   Mode  :character   Mode  :character  
                                                         
                                                         
                                                         

Visualización y preprocesamiento

En primer lugar, vamos a comprobar si existen valores nulos en el dataset. Para ello, utilizaremos la función colSums(is.na(mushroom)), que nos devolverá la suma de valores nulos de cada variable.

colSums(is.na(mushroom))
               class         cap.diameter            cap.shape          cap.surface            cap.color 
                   0                    0                    0                    0                    0 
does.bruise.or.bleed      gill.attachment         gill.spacing           gill.color          stem.height 
                   0                    0                    0                    0                    0 
          stem.width            stem.root         stem.surface           stem.color            veil.type 
                   0                    0                    0                    0                    0 
          veil.color             has.ring            ring.type    spore.print.color              habitat 
                   0                    0                    0                    0                    0 
              season 
                   0 

Según el resultado obtenido anteriormente, no existen valores nulos en el dataset. Sin embargo, si observamos el dataset, podemos observar que existen valores vacíos. Para poder trabajar con ellos, y que no nos de problemas a la hora de realizar el preprocesamiento, sustituiremos dichos valores vacíos por NA.

mushroom[mushroom == ""] <- NA

Comprobamos que se han sustituido correctamente los valores vacíos por NA, pudiendo comprobar la cantidad de valores nulos que hay en cada variable. Esta información nos ayudará a decidir si será conveniente eliminar dichas variables o no.

colSums(is.na(mushroom))
               class         cap.diameter            cap.shape          cap.surface            cap.color 
                   0                    0                    0                14120                    0 
does.bruise.or.bleed      gill.attachment         gill.spacing           gill.color          stem.height 
                   0                 9884                25063                    0                    0 
          stem.width            stem.root         stem.surface           stem.color            veil.type 
                   0                51538                38124                    0                57892 
          veil.color             has.ring            ring.type    spore.print.color              habitat 
               53656                    0                 2471                54715                    0 
              season 
                   0 

Podemos observar que existen 5 variables donde más del 50% de los valores son nulos. Estas son: stem.surface, veil.color, spore.print.color, stem.root, veil.type.

En este caso, decidimos eliminar aquellas variables que cuentan con más del 50% de sus valores faltantes. La razón de esta decisión es que, si decidimos imputar los valores faltantes, estaríamos inventando demasiados datos. Imputar valores faltantes significa reemplazar los valores faltantes con algún valor que estimemos adecuado. Sin embargo, si una variable tiene más del 50% de sus valores faltantes, significa que estaríamos reemplazando más de la mitad de los valores de esa variable. Esto nos llevaría a tener un conjunto de datos con demasiados valores inventados, lo que podría afectar la precisión de nuestro análisis. Para ello, en primer lugar, obtendremos el nombre de las columnas que queremos eliminar.

nacols <- colnames(mushroom)[colSums(is.na(mushroom)) > nrow(mushroom) / 2]
print(nacols)
[1] "stem.root"         "stem.surface"      "veil.type"         "veil.color"        "spore.print.color"

A continuación, eliminaremos dichas columnas del dataset.

mushroom <- mushroom[, !names(mushroom) %in% nacols]

Comprobamos que se han eliminado correctamente las columnas.

print(colnames(mushroom))
 [1] "class"                "cap.diameter"         "cap.shape"            "cap.surface"          "cap.color"           
 [6] "does.bruise.or.bleed" "gill.attachment"      "gill.spacing"         "gill.color"           "stem.height"         
[11] "stem.width"           "stem.color"           "has.ring"             "ring.type"            "habitat"             
[16] "season"              

Para poder analizar de forma específica cada variable, separamos las variables numéricas de las categóricas.

colsnames <- colnames(mushroom)
numerical_features <- c("cap.diameter", "stem.height", "stem.width")
categorical_features <- colsnames[!colsnames %in% numerical_features]
print(categorical_features)
 [1] "class"                "cap.shape"            "cap.surface"          "cap.color"            "does.bruise.or.bleed"
 [6] "gill.attachment"      "gill.spacing"         "gill.color"           "stem.color"           "has.ring"            
[11] "ring.type"            "habitat"              "season"              
print(numerical_features)
[1] "cap.diameter" "stem.height"  "stem.width"  

Comenzamos analizando las variables categóricas. Para ello visualizamos la distribución de las variables categóricas a través de histogramas. Observaremos los posibles valores de cada variable categórica junto con su frecuencia de aparición en el dataset.

for (i in categorical_features) {
  print(ggplot(mushroom, aes_string(x = i)) +
    geom_bar(fill = "#7fd6d9") +
    geom_text(stat = "count", aes(label = scales::percent(..count.. / nrow(mushroom)), vjust = -0.25)) +
    labs(x = i, y = "Percentage") +
    theme(axis.text.x = element_text(angle = 90, hjust = 1)))
}

Tras visualizar los histogramas de cada variable, obtenemos las siguientes conclusiones:

A continuación, seguiremos con el análisis de las variables numéricas, donde visualizaremos su distribución respecto a la clase a través de una gráfica generada con “featurePlot”. Esta requiere que la variable objetivo sea de tipo factor, por lo que hacemos la conversión.

mushroom$class <- as.factor(mushroom$class)
featurePlot(x = mushroom[, numerical_features], y = mushroom$class, plot = "strip")

Con la gráfica anterior, se puede observar que para valores altos de cap.diameter, stem.height y stem.width, la probabilidad de que la clase sea “e” es mayor que la de “p”. Por lo tanto, podemos deducir que si una seta es de tamaño grande, su probabilidad de ser comestible es bastante mayor.

Anteriormente hemos comentado que la variable “ring.type” tiene 8 posibles valores, pero en casi un 80% de los casos, el valor es “f”. Para evaluar su posible eliminación o la de alguna otra variable, se puede utilizar la función “nearZeroVar”. Esta función devuelve un vector con los índices de las variables que tienen una varianza cercana a 0.

near_zero_col <- nearZeroVar(mushroom, saveMetrics = FALSE)
colnames(mushroom)[c(near_zero_col)]
[1] "ring.type"

Como suponíamos, la variable “ring.type” es la que tiene una varianza cercana a 0, y por tanto podría ser eliminada. A continuación, visualizaremos la correlación existente con la variable dependiente “class” para tomar una decisión.

print(ggplot(mushroom, aes_string(x = "ring.type")) +
  geom_bar(aes(fill = class)))

Tras visualizar la gráfica anterior, se puede observar que la distribución de la variable dependiente “class” es similar en casi todos los valores de “ring.type”. Sin embargo, en el caso de “ring.type” = “z” y “ring.type” = “m”, la distribución de la variable dependiente “class” es diferente. Por lo tanto, se decide finalemnte mantener la variable “ring.type”.

Imputación de valores nulos

Como se ha comentado anteriormente, los valores nulos de las variables categóricas se imputarán a través de la moda de cada variable. Este proceso lo hacemos de forma manual, ya que la función preProcess() de la librería caret no permite imputar valores nulos de variables categóricas.

for (i in categorical_features) {
  mushroom[, i][is.na(mushroom[, i])] <- names(which.max(table(mushroom[, i])))
}

Comprobamos que ya no existen valores nulos en las variables categóricas.

colSums(is.na(mushroom))
               class         cap.diameter            cap.shape          cap.surface            cap.color 
                   0                    0                    0                    0                    0 
does.bruise.or.bleed      gill.attachment         gill.spacing           gill.color          stem.height 
                   0                    0                    0                    0                    0 
          stem.width           stem.color             has.ring            ring.type              habitat 
                   0                    0                    0                    0                    0 
              season 
                   0 

Escalado de variables numéricas

Con el objetivo de que todas las variables tengan la misma escala y evitar que una variable tenga más peso que otra, se escalarán las variables numéricas. Para realizar este escalado, se utilizará la función preProcess() de la librería caret. Esta función devuelve un objeto de tipo “preProcess” que contiene la información necesaria para escalar las variables numéricas. A continuación, se realizará el escalado y se sustituirán las variables numéricas originales por las escaladas.

range_numeric <- preProcess(mushroom[, numerical_features], method = c("range"))
mushroom[, numerical_features] <- predict(range_numeric, newdata = mushroom[, numerical_features])
str(mushroom)
'data.frame':   61069 obs. of  16 variables:
 $ class               : Factor w/ 2 levels "e","p": 2 2 2 2 2 2 2 2 2 2 ...
 $ cap.diameter        : num  0.24 0.262 0.221 0.223 0.23 ...
 $ cap.shape           : chr  "x" "x" "x" "f" ...
 $ cap.surface         : chr  "g" "g" "g" "h" ...
 $ cap.color           : chr  "o" "o" "o" "e" ...
 $ does.bruise.or.bleed: chr  "f" "f" "f" "f" ...
 $ gill.attachment     : chr  "e" "e" "e" "e" ...
 $ gill.spacing        : chr  "c" "c" "c" "c" ...
 $ gill.color          : chr  "w" "w" "w" "w" ...
 $ stem.height         : num  0.5 0.53 0.525 0.465 0.487 ...
 $ stem.width          : num  0.164 0.175 0.171 0.154 0.166 ...
 $ stem.color          : chr  "w" "w" "w" "w" ...
 $ has.ring            : chr  "t" "t" "t" "t" ...
 $ ring.type           : chr  "g" "g" "g" "p" ...
 $ habitat             : chr  "d" "d" "d" "d" ...
 $ season              : chr  "w" "u" "w" "w" ...

Análisis de datos

Análisis aprendizaje supervisado

Para trabajar con el dataset mediante el aprendizaje supervisado; es decir, utilizando datos que son etiquetados mediante la intervención de un ser humano, utilizaremos diferentes tipos de clasificadores, algunos de ellos ya trabajados durante las clases prácticas de la asignatura y otros que eran desconocidos para nosotros y sobre los cuales hemos tenido que investigar anteriormente sobre su funcionamiento en R. Los algoritmos de clasificación que hemos seleccionado y los cuales vamos a aplicar son:

  1. Regresión logística.

  2. KNN o el Vecino más cercano.

  3. Árboles de decisión.

  4. Random Forest.

  5. MSV o Máquina de Soporte Vectorial.

Una vez aplicados cada uno de los clasificadores, los compararemos entre sí y seleccionaremos el o los algoritmos que mayor precisión proporcionen sin llegar al sobreajuste, tratando de buscar que el resultado final sea generalizado para los datos.

En primer lugar, ante de comenzar a aplicar los clasificadores, dividiremos el dataset en dos conjuntos: el primer conjunto será el de entrenamiento o training y el segundo será el conjunto de prueba o test. El conjunto de entrenamiento lo utilizaremos para entrenar los distintos modelos y el conjunto de prueba lo utilizaremos para evaluar cada uno de ellos una vez obtenidos.

library(caTools)
set.seed(18)

split <- sample.split(mushroom$class, SplitRatio = 0.8)
training_set <- subset(mushroom, split == TRUE)
test_set <- subset(mushroom, split == FALSE)

table(training_set$class)

    e     p 
21745 27110 
table(test_set$class)

   e    p 
5436 6778 

Regresión Logística

La regresión logística permite predecir el resultado de una variable categórica en función de las variables independientes o predictoras. A continuación se muestra un resumen del conjunto de datos de entrenamiento con el cual vamos a trabajar una vez aplicada la función glm().

rl_classiffier <- glm(class ~ ., family = binomial, data = training_set)
summary(rl_classiffier)

Call:
glm(formula = class ~ ., family = binomial, data = training_set)

Deviance Residuals: 
     Min        1Q    Median        3Q       Max  
-2.98113  -0.77757   0.00045   0.75561   2.99676  

Coefficients: (2 not defined because of singularities)
                        Estimate Std. Error z value Pr(>|z|)    
(Intercept)           -16.146622 310.494270  -0.052 0.958526    
cap.diameter           -3.097299   0.273141 -11.340  < 2e-16 ***
cap.shapec             -1.587320   0.089853 -17.666  < 2e-16 ***
cap.shapef             -1.548574   0.056840 -27.244  < 2e-16 ***
cap.shapeo             -0.505473   0.102060  -4.953 7.32e-07 ***
cap.shapep             -1.355581   0.077525 -17.486  < 2e-16 ***
cap.shapes             -1.914206   0.067428 -28.389  < 2e-16 ***
cap.shapex             -1.587070   0.052730 -30.098  < 2e-16 ***
cap.surfacee            0.777609   0.081079   9.591  < 2e-16 ***
cap.surfaceg           -0.469069   0.067815  -6.917 4.62e-12 ***
cap.surfaceh           -0.869361   0.063758 -13.635  < 2e-16 ***
cap.surfacei            1.878605   0.116592  16.113  < 2e-16 ***
cap.surfacek            3.205155   0.106739  30.028  < 2e-16 ***
cap.surfacel           -1.338110   0.100947 -13.256  < 2e-16 ***
cap.surfaces           -0.913831   0.059362 -15.394  < 2e-16 ***
cap.surfacet           -0.037125   0.050461  -0.736 0.461905    
cap.surfacew           -0.483820   0.084640  -5.716 1.09e-08 ***
cap.surfacey           -0.579375   0.064113  -9.037  < 2e-16 ***
cap.colore              1.986990   0.109951  18.072  < 2e-16 ***
cap.colorg              0.582318   0.108490   5.367 7.98e-08 ***
cap.colork              1.553011   0.131690  11.793  < 2e-16 ***
cap.colorl             -0.129820   0.140094  -0.927 0.354103    
cap.colorn              0.374666   0.099619   3.761 0.000169 ***
cap.coloro              1.627975   0.112745  14.439  < 2e-16 ***
cap.colorp              1.051060   0.127472   8.245  < 2e-16 ***
cap.colorr              2.925006   0.136340  21.454  < 2e-16 ***
cap.coloru              1.538956   0.121348  12.682  < 2e-16 ***
cap.colorw              0.861936   0.104891   8.217  < 2e-16 ***
cap.colory              0.615058   0.102936   5.975 2.30e-09 ***
does.bruise.or.bleedt  -0.147991   0.038250  -3.869 0.000109 ***
gill.attachmentd        0.644857   0.046979  13.726  < 2e-16 ***
gill.attachmente       -0.922859   0.055651 -16.583  < 2e-16 ***
gill.attachmentf        0.816396   0.136750   5.970 2.37e-09 ***
gill.attachmentp       -2.450165   0.060717 -40.354  < 2e-16 ***
gill.attachments        0.142043   0.052091   2.727 0.006395 ** 
gill.attachmentx        0.089014   0.043392   2.051 0.040231 *  
gill.spacingd          -0.425196   0.036863 -11.534  < 2e-16 ***
gill.spacingf                 NA         NA      NA       NA    
gill.colore             2.304077   0.156835  14.691  < 2e-16 ***
gill.colorf                   NA         NA      NA       NA    
gill.colorg             0.833125   0.124899   6.670 2.55e-11 ***
gill.colork             0.854961   0.136405   6.268 3.66e-10 ***
gill.colorn             1.350254   0.119303  11.318  < 2e-16 ***
gill.coloro             1.131719   0.123598   9.156  < 2e-16 ***
gill.colorp             0.848246   0.122841   6.905 5.01e-12 ***
gill.colorr             0.896493   0.144108   6.221 4.94e-10 ***
gill.coloru             1.319431   0.145603   9.062  < 2e-16 ***
gill.colorw             0.819802   0.115149   7.120 1.08e-12 ***
gill.colory             1.818426   0.119373  15.233  < 2e-16 ***
stem.height             3.580359   0.196103  18.258  < 2e-16 ***
stem.width             -0.455339   0.225084  -2.023 0.043076 *  
stem.colore            17.897727 310.494233   0.058 0.954033    
stem.colorf            35.399323 334.067154   0.106 0.915610    
stem.colorg            15.906809 310.494228   0.051 0.959142    
stem.colork            19.545176 310.494262   0.063 0.949807    
stem.colorl            15.591779 310.494286   0.050 0.959950    
stem.colorn            17.491009 310.494224   0.056 0.955077    
stem.coloro            16.096825 310.494231   0.052 0.958654    
stem.colorp            19.131727 310.494251   0.062 0.950868    
stem.colorr            17.537012 310.494253   0.056 0.954959    
stem.coloru            17.419638 310.494223   0.056 0.955260    
stem.colorw            16.082601 310.494224   0.052 0.958691    
stem.colory            17.437089 310.494226   0.056 0.955215    
has.ringt              -0.006793   0.050561  -0.134 0.893124    
ring.typef             -0.911592   0.084553 -10.781  < 2e-16 ***
ring.typeg             -0.499679   0.101534  -4.921 8.60e-07 ***
ring.typel             -0.167323   0.100958  -1.657 0.097449 .  
ring.typem            -20.187930 230.159347  -0.088 0.930105    
ring.typep              0.894990   0.103503   8.647  < 2e-16 ***
ring.typer             -0.492646   0.105898  -4.652 3.29e-06 ***
ring.typez             16.741684  78.753199   0.213 0.831651    
habitatg                0.548824   0.041040  13.373  < 2e-16 ***
habitath                0.199731   0.065306   3.058 0.002225 ** 
habitatl               -0.501569   0.054900  -9.136  < 2e-16 ***
habitatm                0.103008   0.067655   1.523 0.127870    
habitatp               17.002090 230.326776   0.074 0.941156    
habitatu              -17.054326 378.536784  -0.045 0.964065    
habitatw              -17.436003 215.700665  -0.081 0.935574    
seasons                -1.618439   0.069225 -23.379  < 2e-16 ***
seasonu                 0.181688   0.025335   7.171 7.42e-13 ***
seasonw                -1.277528   0.049110 -26.014  < 2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 67137  on 48854  degrees of freedom
Residual deviance: 45160  on 48776  degrees of freedom
AIC: 45318

Number of Fisher Scoring iterations: 16

La función aplicada, glm(), obtiene los valores residuales del modelo y los coeficientes de ajuste para cada una de las variables independientes. Además, se obtiene el p-value correspondiente para cada una de ellas. En el resumen mostrado mediante la función summary(), se pueden observar variables con dos o tres asteriscos, las cuales aportan bastante relevancia al modelo como predictores; sin embargo, las variables que poseen un solo asterisco o incluso ninguno, significa que apenas aportan relevancia a los resultados. A continuación, procedemos a predecir las clases del conjunto de entrenamiento y de validación. Tomando como umbral ‘0,5’, dividiremos los champiñones en comestibles y venenosos, de forma que si la probabilidad queda por encima de dicho umbral significará que el champiñón es comestible, y de lo contrario, si el resultado se mantiene por debajo, significará que el champiñón es venenoso. Para acabar, formaremos la matriz de confusión con los resultados de las predicciones para proceder a su análisis y valoración.

pred_train <- predict(rl_classiffier, newdata = training_set, type = "response")
Warning: prediction from a rank-deficient fit may be misleading
pred_train <- ifelse(pred_train > 0.5, "p", "e")
pred_train <- factor(pred_train, levels = c("e", "p"), labels = c("e", "p"))

confusion_m <- table(training_set$class, pred_train)
print(confusion_m)
   pred_train
        e     p
  e 16489  5256
  p  5573 21537
accuracy <- sum(diag(confusion_m)) / sum(confusion_m)
print(accuracy)
[1] 0.7783441

Una vez obtenido el resultado mostrado mediante la matriz de confusión, se observa que la predicción posee una precisión del 77,8%. A continuación, se muestra el resultado de aplicar el mismo proceso anterior sobre el conjunto de prueba.

pred_test <- predict(rl_classiffier, newdata = test_set, type = "response")
Warning: prediction from a rank-deficient fit may be misleading
pred_test <- ifelse(pred_test > 0.5, "p", "e")
pred_test <- factor(pred_test, levels = c("e", "p"), labels = c("e", "p"))

confusion_m <- table(test_set$class, pred_test)
print(confusion_m)
   pred_test
       e    p
  e 4087 1349
  p 1420 5358
accuracy_rl <- sum(diag(confusion_m)) / sum(confusion_m)
print(accuracy_rl)
[1] 0.7732929

Una vez obtenido este segundo resultado, podemos observar que hay una gran similitud entre los resultados obtenidos para ambos conjuntos de datos, resultando esta vez en un 77,3%. Antes de acabar de aplicar el clasificador de regresión logística, vamos a proceder a graficar la curva ROC, la cual nos aporta mayor visualización de la relación entre los falsos y verdaderos positivos.

library(ROCR)
pred_rl_roc <- prediction(as.numeric(pred_test), as.numeric(test_set$class))
perf_rl_roc <- performance(pred_rl_roc, "tpr", "fpr")
perf_rl_auc <- performance(pred_rl_roc, "auc")

print(perf_rl_auc@y.values[[1]])
[1] 0.7711691
plot(perf_rl_roc, col = "lightblue", lwd = 5)  

Observando la curva ROC resultante podemos comentar que se mantiene por encima de la diagonal, lo que es buena señal, pero se aproxima a ella, pudiendo haber proporcionado resultados mejores resulta ser un modelo bastante generalizado.

k-NN

Mediante el algoritmo de clasificación llamado k-NN se asigna una nueva observación a la clase más común entre sus “k” vecinos más cercanos en el espacio de características. Antes de nada, para poder aplicar el algoritmo k-NN, debemos transformar las variables categóricas en numéricas.

mushroom_num <- dummyVars(" ~ .", data = mushroom, fullRank = TRUE) %>% predict(mushroom)
mushroom_num <- as.data.frame(mushroom_num)

A continuación, se visualizan las variables categóricas ya codificadas y las dimensiones del dataset.

dim_mushroom <- dim(mushroom_num)
print(dim_mushroom)
[1] 61069    81
str(mushroom_num)
'data.frame':   61069 obs. of  81 variables:
 $ class.p              : num  1 1 1 1 1 1 1 1 1 1 ...
 $ cap.diameter         : num  0.24 0.262 0.221 0.223 0.23 ...
 $ cap.shapec           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.shapef           : num  0 0 0 1 0 0 1 0 1 1 ...
 $ cap.shapeo           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.shapep           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.shapes           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.shapex           : num  1 1 1 0 1 1 0 1 0 0 ...
 $ cap.surfacee         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfaceg         : num  1 1 1 0 0 1 0 0 1 1 ...
 $ cap.surfaceh         : num  0 0 0 1 1 0 1 1 0 0 ...
 $ cap.surfacei         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfacek         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfacel         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfaces         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfacet         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfacew         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfacey         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colore           : num  0 0 0 1 0 0 0 1 0 1 ...
 $ cap.colorg           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colork           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colorl           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colorn           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.coloro           : num  1 1 1 0 1 1 1 0 1 0 ...
 $ cap.colorp           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colorr           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.coloru           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colorw           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colory           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ does.bruise.or.bleedt: num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.attachmentd     : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.attachmente     : num  1 1 1 1 1 1 1 1 1 1 ...
 $ gill.attachmentf     : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.attachmentp     : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.attachments     : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.attachmentx     : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.spacingd        : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.spacingf        : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colore          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorf          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorg          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colork          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorn          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.coloro          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorp          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorr          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.coloru          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorw          : num  1 1 1 1 1 1 1 1 1 1 ...
 $ gill.colory          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.height          : num  0.5 0.53 0.525 0.465 0.487 ...
 $ stem.width           : num  0.164 0.175 0.171 0.154 0.166 ...
 $ stem.colore          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorf          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorg          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colork          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorl          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorn          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.coloro          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorp          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorr          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.coloru          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorw          : num  1 1 1 1 1 1 1 1 1 1 ...
 $ stem.colory          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ has.ringt            : num  1 1 1 1 1 1 1 1 1 1 ...
 $ ring.typef           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ ring.typeg           : num  1 1 1 0 0 0 1 0 0 0 ...
 $ ring.typel           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ ring.typem           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ ring.typep           : num  0 0 0 1 1 1 0 1 1 1 ...
 $ ring.typer           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ ring.typez           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatg             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitath             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatl             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatm             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatp             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatu             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatw             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ seasons              : num  0 0 0 0 0 0 0 0 0 0 ...
 $ seasonu              : num  0 1 0 0 0 1 0 1 0 0 ...
 $ seasonw              : num  1 0 1 1 1 0 1 0 0 1 ...

Debido al hecho de necesitar la transformación de las variables categóricas en numéricas, procedemos en este paso a dividir los datos en los conjuntos de entrenamiento y de validación. En este caso, el valor 0 corresponde con los champiñones comestibles y el valor 1 con los champiñones venenosos.

library(caTools)
set.seed(18)

split <- sample.split(mushroom_num$class, SplitRatio = 0.8)
training_set_num <- subset(mushroom_num, split == TRUE)
test_set_num <- subset(mushroom_num, split == FALSE)

table(training_set_num$class)

    0     1 
21745 27110 
table(test_set_num$class)

   0    1 
5436 6778 

El siguiente paso será determinar el valor óptimo de k antes de proceder a aplicar la función. Para ello, utilizaremos el resultado que nos proporciona el cálculo de la raíz cuadrada del número de observaciones del conjunto de entrenamiento.


nrows_class <- NROW(training_set_num) 
k <- sqrt(nrows_class)
k <- round(k)
k
[1] 221

Una vez hemos obtenido el valor de k, procedemos a realizar las predicciones. Para llevar a cabo la aplicación del clasificador llamado ‘el vecino más cercano’, haremos uso de la función knn() de la librería class.

library(class)
set.seed(18)
pred_knn <- knn(train = training_set_num[, -1], test = test_set_num[, -1], cl = training_set_num$class, k = k)
summary(pred_knn)
   0    1 
5288 6926 

Una vez obtenidos los resultados de las predicciones en este caso, podemos de igual forma construir la matriz de confusión.

confusion_m <- table(test_set_num$class, pred_knn)
confusion_m
   pred_knn
       0    1
  0 5233  203
  1   55 6723
accuracy_knn <- sum(diag(confusion_m)) / sum(confusion_m)
accuracy_knn
[1] 0.9788767

Como resultado final de la matriz de confusión, obtenemos una precisión resultante del 97%, la cual mejora respecto al algoritmo de clasificación anterior, aunque tendiendo a poseer mayor sobreajuste. De forma más visual, obtenemos a continuación la gráfica de la curva ROC y el cálculo del área bajo la misma.

library(ROCR)
pred_knn_roc <- prediction(as.numeric(pred_knn), as.numeric(test_set_num$class))
perf_knn_roc <- performance(pred_knn_roc, "tpr", "fpr")
perf_knn_auc <- performance(pred_knn_roc, "auc")

print(perf_knn_auc@y.values[[1]])
[1] 0.9772709
plot(perf_knn_roc, col = "lightblue", lwd = 5)  

Una vez obtenemos la curva ROC y su área, observamos que el resultado es muy positivo en cuanto a precisión de los resultados, ya que como se puede observar gráficamente se aleja de la diagonal.

Clasificación con Árbol de Decisión

Los árboles de decisión se basan en la construcción de reglas lógicas (divisiones de los datos entre rangos o condiciones) a partir de los datos de entrada. Para trabajar con este clasificador comenzamos aplicando la función del árbol de decisión, rpart(), sobre el conjunto de datos de entrenamiento como se muestra a continuación.

library(rpart)
set.seed(18)
dt_classiffier <- rpart(class ~ ., data = training_set)

Una vez obtenido el resultado, lo graficaremos para facilitar así el análisis del resultado.

library(rpart.plot)
rpart.plot(dt_classiffier)

Construimos la matriz de confusión con los resultados obtenidos anteriormente para este caso.

pred_dt <- predict(dt_classiffier, newdata = test_set, type = "class")

confusion_m <- table(test_set$class, pred_dt)
confusion_m
   pred_dt
       e    p
  e 4257 1179
  p  804 5974
accuracy_dt <- sum(diag(confusion_m)) / sum(confusion_m)
accuracy_dt
[1] 0.8376453

Una vez tenemos el resultado para el valor de precisión en la predicción aplicando el árbol de decisión, 83%, procedemos a construir la gráfica de la curva ROC y a calcular el valor correspondiente al AUC. En este caso, continúa siendo mejor resultado que el obtenido mediante la regresión logística, puesto que resulta en una mayor precisión, pero al igual que el algoritmo de k-NN, tiende a ser más sobreajustado.

library(ROCR)
pred_dt_roc <- prediction(as.numeric(pred_dt), as.numeric(test_set$class))
perf_dt_roc <- performance(pred_dt_roc, "tpr", "fpr")
perf_dt_auc <- performance(pred_dt_roc, "auc")

print(perf_dt_auc@y.values[[1]])
[1] 0.8322468
plot(perf_dt_roc, col = "lightblue", lwd = 5)  

Clasificador Random Forest

El algoritmo de Random Forest trabaja mediante la combinación de árboles predictores tal que cada árbol depende de los valores de un vector aleatorio. Comenzamos aplicando dicho clasificador mediante la función llamada de la misma forma; es decir, randomForest(). En esta función, el valor correspondiente al parámetro llamado ‘ntree’ indica la cantidad de árboles de decisión que formarán parte del clasificador.

library(randomForest)
set.seed(18)
rf_classiffier <- randomForest(class ~ ., data = training_set, ntree = 250)

Mediante su gráfica, vamos a proceder a comparar los errores en función del aumento del número de árboles; es decir, cuanto más vaya aumentando el número de árboles hasta un umbral determinado, menor cantidad de errores poseerá la predicción.

plot(rf_classiffier)

Calculamos las predicciones sobre el conjunto de datos de prueba y construimos la matriz de confusión.

pred_rf <- predict(rf_classiffier, newdata = test_set, type = "class")

confusion_m <- table(test_set$class, pred_rf)
confusion_m
   pred_rf
       e    p
  e 5431    5
  p    0 6778
accuracy_rf <- sum(diag(confusion_m)) / sum(confusion_m)
accuracy_rf
[1] 0.9995906

Una vez obtenido el valor de la precisión para este caso, definimos la curva ROC y procedemos a realizar el cálculo del área bajo la curva. Mediante este último paso se puede observar su tan alta precisión, la cual indica demasiado sobreajuste sin ser conveniente.

library(ROCR)
pred_rf_roc <- prediction(as.numeric(pred_rf), as.numeric(test_set$class))
perf_rf_roc <- performance(pred_rf_roc, "tpr", "fpr")
perf_rf_auc <- performance(pred_rf_roc, "auc")

print(perf_rf_auc@y.values[[1]])
[1] 0.9995401
plot(perf_rf_roc, col = "lightblue", lwd = 5)  

Kernel SVM Classifier

El clasificador de la máquina vectorial, encuentra la curva que es capaz de separar y clasificar los datos de entrenamiento garantizando que la separación entre ésta y ciertas observaciones del conjunto de entrenamiento resulte ser lo mayor posible. Para llevar a cabo la aplicación del clasificador llamado ‘Máquina de Soporte Vectorial’ hacemos uso de la función svm(). En dicha función, los valores de los parámetros ‘type’ y ‘kernel’ hacen referencia al tipo de clasificador lo que significa que el kernel será de tipo radial y gaussiano.

library(e1071)
set.seed(18)
svm_classiffier <- svm(class ~ .,
  data = training_set,
  type = "C-classification", kernel = "radial"
)

A continuación, se calcula la predicción y se construye la matríz de confusión, las cuales resultan ser las siguientes.

pred_svm <- predict(svm_classiffier, newdata = test_set, type = "class")

confusion_m <- table(test_set$class, pred_svm)
confusion_m
   pred_svm
       e    p
  e 5129  307
  p  240 6538
accuracy_svm <- sum(diag(confusion_m)) / sum(confusion_m)
accuracy_svm
[1] 0.9552153

Como se puede observar, el valor de la precisión en la predicción en este caso resulta ser del 95%, un resultado bueno que no muestra señales de sobreajuste. Para finalizar, construimos la curva ROC correspondiente en este caso y calculamos su área.

library(ROCR)
pred_svm_roc <- prediction(as.numeric(pred_svm), as.numeric(test_set$class))
perf_svm_roc <- performance(pred_svm_roc, "tpr", "fpr")
perf_svm_auc <- performance(pred_svm_roc, "auc")

print(perf_svm_auc@y.values[[1]])
[1] 0.954058
plot(perf_svm_roc, col = "lightblue", lwd = 5)  

Conclusiones

Una vez aplicados los cinco distintos métodos de clasificación sobre nuestro dataset llamado ‘mushroom’ tras haber realizado antes su preprocesamiento, podemos concluir diciendo que el clasificador de Random Forest es el que ha resultado poseer un mayor valor en la precisión de la predicción en la clasificación y, por lo tanto, un menor valor para el error de predicción. Sin embargo, al aplicar este algoritmo hemos obtenido un mayor sobreajuste, el cual no beneficia al modelo ya que se busca que los resultados obtenidos sean precisos, pero también generalizados para los datos. Por esta razón, consideramos que resulta más beneficioso sacrificar parte del valor de precisión, teniendo en cuenta algunos valores de falsos positivos y falsos negativos, como ocurre en el caso de los clasificadores de la Máquina de Soporte Vectorial o k-NN, aprovechando así su capacidad de mayor generalización. Sobre los gráficos que se muestran a continuación se pueden comparar los distintos niveles de precisión y AUC para cada uno de los diferentes clasificadores que han sido aplicados.

accuracy_comp <- matrix(c(accuracy_rl, accuracy_knn, accuracy_dt, accuracy_rf, accuracy_svm), ncol = 5)

barplot(accuracy_comp,
  main = "Accuracy Comparison",
  xlab = "Accuracy (%)",
  ylab = "Method",
  names.arg = c("RL", "K-NN", "DT", "RF", "SVM"),
  col = "#7fd6d9"
)

perf_auc <- matrix(c(perf_rl_auc@y.values[[1]], perf_knn_auc@y.values[[1]], perf_dt_auc@y.values[[1]], perf_rf_auc@y.values[[1]], perf_svm_auc@y.values[[1]]), ncol = 5)

barplot(perf_auc,
  main = "AUC Comparison",
  xlab = "AUC (%)",
  ylab = "Method",
  names.arg = c("RL", "K-NN", "DT", "RF", "SVM"),
  col = "#7fd6d9"
)

Análisis aprendizaje no supervisado

En este apartado se analizará el dataset a través de algoritmos de aprendizaje no supervisado. En concreto, se probarán los algoritmos k-means y clustering jerárquico. Para ambos algoritmos, se seguirá el siguiente esquema:

Para ambos algoritmos se llevará a cabo el siguiente proceso:

  1. Se representará gráficamente la distribución inicial de los datos en el dataset.
  2. Se determinará el número óptimo de clústeres a utilizar para dividir los datos de forma adecuada.
  3. Se representará gráficamente la distribución de los datos en función del número de clústeres elegido.
  4. Se calculará el promedio de cada una de las variables en el dataset para cada uno de los clústeres resultantes.
  5. Finalmente, y gracias a tener información a priori de la clase de cada champiñón, se calculará el “accuracy” del algoritmo, midiendo cómo de bien se está realizando la tarea de dividir los datos en clústeres de forma adecuada.

Para poder trabajar con algoritmos no supervisados será necesario que las variables sean numéricas. Para ello, se eliminarán las variables categóricas del dataset.

numerical_columns <- mushroom[, numerical_features]

Antes de comenzar con los algoritmos no supervisados, representaremos de forma gráfica la distribución inicial de los datos a través de un diagrama de dispersión 3D, dónde cada punto representa un champiñón y las variables que se representan son el diámetro del sombrero, la altura del tallo y el ancho del tallo. Cada variable está normalizada entre 0 y 1.

df <- as.data.frame(numerical_columns)

plot_ly(df,
  x = ~cap.diameter, y = ~stem.height,
  z = ~stem.width
) %>%
  add_markers(size = 1.5)

K-means

El algoritmo k-means es un método de clustering que permite dividir un conjunto de datos en k grupos o clústeres de manera que los puntos dentro de un mismo clústeres sean similares entre sí y diferentes a los puntos de los demás clústeres. La función kmeans() de la librería clústeres una implementación del algoritmo k-means en R.

Para utilizar la función kmeans(), es necesario especificar el número de clústeres que se desean obtener, que se indica a través del parámetro “centers”.

Por otro lado, el parámetro “nstar” indica el número de veces que se desea realizar el proceso de clustering. Cada vez que se ejecuta el proceso, se utiliza un conjunto diferente de semillas iniciales para los centroides de los clústeres y se obtiene un resultado diferente. Al especificar un valor para “nstart” mayor que 1, se obtienen varios resultados diferentes y se selecciona el que minimiza la suma de cuadrados total. Por tanto, establecemos “nstart” a 20 para que se realice el proceso 20 veces y se obtenga un resultado más robusto.

Una vez que se ha ejecutado la función con un determinado valor de “centers”, se puede calcular la suma de cuadrados internos (within groups sum of squares) para ese valor de “centers”. La suma de cuadrados internos es una medida de la variabilidad de los datos dentro de cada cluster. Cuanto mayor sea la suma de cuadrados internos, más dispersos estarán los datos dentro del clústery, por tanto, menos homogéneo será el cluster.

Para determinar el número óptimo de clústeres, se puede utilizar el método del codo, que consiste en representar la suma de cuadrados internos en función del número de clústeres y seleccionar el número de clústeres en el que se produce un “codo” en la gráfica. Este “codo” suele corresponder al punto en el que la disminución de la suma de cuadrados internos se vuelve más lenta y, por tanto, a partir del cual no se obtienen mejoras significativas en la calidad del clustering.

wss_per_k <- 0
for (i in 1:10) {
  kmeans_aux <- kmeans(numerical_columns, center = i, nstar = 20)
  wss_per_k[i] <- kmeans_aux$tot.withinss
}
Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)
par(mfrow = c(1, 1))
plot(1:10, wss_per_k,
  type = "b",
  xlab = "Number of clusters",
  ylab = "WSS",
)

Como se puede observar en la gráfica anterior, la suma de cuadrados internos disminuye a medida que aumenta el número de clústeres. Sin embargo, a partir de 2 clústeres, la disminución de la suma de cuadrados internos es más pequeña. Por lo tanto, se decide hacer uso 2 clústeres. En este caso específico, tiene sentido utilizar 2 clústeres, ya que conocemos que el dataset es binario.

Una vez determinado el número de clústeres, generamos el modelo de k-means.

km_model <- kmeans(df, center = 2, nstar = 20)

Para poder visualizar los resultados, se añade una nueva columna al dataset con el número de clúster al que pertenece cada observación. Una vez añadida, se puede representar la distribución de los datos en función de los clústeres obtenidos.

df$cluster<- factor(km_model$cluster)

plot_ly(df,
  x = ~cap.diameter, y = ~stem.height,
  z = ~stem.width, color = ~cluster
) %>%
  add_markers(size = 1.5)
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels

Se puede observar que los champiñones de menor tamaño (en diámetro, altura y anchura) pertenecen al clúster 2 y los de mayor tamaño pertenecen al clúster 1.

A continuación, calcularemos el valor promedio de las variables para cada clúster generado con el modelo de k-means. Para ello, utilizaremos la función group_by() de la librería dplyr para agrupar los datos por clúster y la función summarise() para calcular el valor promedio de cada variable. Es importante destacar que, en este caso, los datos están normalizados, por lo que el valor promedio de cada variable no tiene un significado real. Sin embargo, nos permite comparar los valores de cada variable para cada cluster.

grouped_mushroom <- df %>%
  group_by(cluster) %>%
  summarise(
    mean_cap_diameter = mean(cap.diameter),
    mean_stem_height = mean(stem.height),
    mean_stem_width = mean(stem.width)
  )

grouped_mushroom

Observamos que los valores del clúster 1 son mayores que los del clúster 2, lo que indica que los champiñones del clúster 1 son de mayor tamaño que los del clúster 2.

A partir de este momento, hemos decidido modificar el dataset actual debido a que para aplicar técnicas como “silhouette” o “dendrogram” (para el caso de clustering jerárquico) es necesario que el dataset sea de menor tamaño. La función createDataPartition() de la librería caret en R permite dividir un conjunto de datos en dos grupos, uno de entrenamiento y otro de validación, de manera estratificada, es decir, manteniendo la proporción de elementos de cada clase en ambos grupos. En este caso, se está especificando que se desea mantener únicamente el 1% de los datos iniciales para el análisis, lo que implica que se está utilizando createDataPartition() para reducir el tamaño del conjunto de datos en lugar de para dividirlo en dos grupos. Al utilizar createDataPartition() de esta manera, se mantiene la proporción de elementos de cada clase en el conjunto de datos reducido.

Antes de reducir el dataset, es necesario convertir las variables categóricas en numéricas a través de variables dummy. Para ello, utilizamos la función dummyVars() de la librería caret para crear un objeto de tipo dummyVars y la función predict() para crear un nuevo dataset con las variables dummy.

mushroom <- dummyVars(" ~ .", data = mushroom, fullRank = TRUE) %>% predict(mushroom)
mushroom <- as.data.frame(mushroom)

set.seed(42)
split <- createDataPartition(mushroom$class, p = 0.01)
smaller_df <- mushroom[split$Resample1, ]

Comprobamos que la proporción de datos de cada clase se mantiene al hacer la partición.

initial_class_prop <- table(mushroom$class) / nrow(mushroom)
smaller_class_prop <- table(smaller_df$class) / nrow(smaller_df)

print(initial_class_prop)

        0         1 
0.4450867 0.5549133 
print(smaller_class_prop)

        0         1 
0.4386252 0.5613748 

Mostramos de forma gráfica las nuevas proporciones de la variable dependiente “class” en el dataset reducido.

print(ggplot(smaller_df, aes_string(x = smaller_df$class)) +
  geom_bar(fill = "#7fd6d9") +
  geom_text(stat = "count", aes(label = scales::percent(..count.. / nrow(smaller_df)), vjust = -0.25)) +
  labs(x = i, y = "Percentage") +
  theme(axis.text.x = element_text(angle = 90, hjust = 1)))

Una vez reducido el dataset, vamos a eliminar las variables categóricas para poder aplicar técnicas de clustering.

smaller_df <- smaller_df[, numerical_features]
dim(smaller_df)
[1] 611   3

Tras ejecutar la celda anterior se puede observar que el dataset ha pasado a tener 611 observaciones y 3 variables.

Como nos encontramos ante un dataset “nuevo”, en primer lugar, visualizaremos la distribución inicial de los datos a través de una gráfica 3D.

plot_ly(smaller_df,
  x = ~cap.diameter, y = ~stem.height,
  z = ~stem.width
) %>%
  add_markers(size = 1.5)

A continuación, vamos a estudiar cuál sería el número óptimo de clústeres para el dataset reducido haciendo uso de la medida de bondad interna “silhouette”. Para ello, utilizaremos la función fviz_nbclust de factoextra.

La medida silhouette toma valores entre -1 y 1. Un valor cercano a 1 indica que el punto está bien asignado al clúster y los puntos del clúster son muy similares entre sí. Un valor cercano a 0 indica que el punto está en un “área gris” y no está claramente asignado a ninguno de los dos clústeres. Un valor cercano a -1 indica que el punto está mal asignado al clúster y sería más apropiado para otro clúster.

La función fviz_nbclust() de la librería factoextra en R permite visualizar la medida silhouette para diferentes valores de “k” (número de clústeres) y ayudar a determinar el número óptimo de clústeres. Al utilizar esta función, se puede obtener un gráfico en el que se representa la medida silhouette en función del número de clústeres y seleccionar el valor de “k” en el que se obtiene el mayor valor de silhouette.

fviz_nbclust(smaller_df, FUNcluster = kmeans, method = "silhouette")

Según la gráfica, podemos afirmar que el número óptimo de clústeres es 2, ya que es el valor de “k” que maximiza la medida silhouette. También podemos observar que el valor de silhouette para “k” = 3 es cercano al obtenido para “k” = 2. Esto puede ser debido a que nuestro dataset, pese a ser binario, cuenta con datos muy dispersos.

Por último, visualizamos la distribución de los datos en función de los clústeres obtenidos.

km_sm_model <- kmeans(smaller_df, center = 2, nstart = 20)
cluster <- factor(km_sm_model$cluster)

plot_ly(smaller_df,
  x = ~cap.diameter, y = ~stem.height,
  z = ~stem.width, color = ~cluster
) %>%
  add_markers(size = 1.5)
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels

Por último, calculamos el valor promedio de las variables para cada clúster generado con el modelo de k-means para el dataset reducido.

grouped_sm_mushroom <- smaller_df %>%
  mutate(cluster = cluster) %>%
  group_by(cluster) %>%
  summarise(
    mean_cap_diameter = mean(cap.diameter),
    mean_stem_height = mean(stem.height),
    mean_stem_width = mean(stem.width)
  )
grouped_sm_mushroom

Clustering jerárquico

Clustering jerárquico es un tipo de algoritmo de clustering que se utiliza para dividir a un conjunto de datos en grupos (clústeres) de forma que los datos en el mismo clústersean similares entre sí. La función hclust es una función en R que se utiliza para realizar clustering jerárquico.

Antes de aplicar el algoritmo hclust, es necesario calcular las distancias entre los puntos del conjunto de datos, para ello, utilizaremos la función dist de R. La función dist() calcula la distancia entre los puntos del conjunto de datos y devuelve una matriz de distancias. Por defecto, la función dist() utiliza la distancia euclídea para calcular las distancias entre los puntos del conjunto de datos.

Cabe destacar, que la función hclust hace uso por defecto del cáculo de distancia entre clústeres basado en el método de “Complete”. Este método de cálculo de distancia entre clústeres se basa en la distancia entre los puntos más lejanos de cada cluster.

distance <- dist(smaller_df)
hc_model <- hclust(distance)

Representamos el dendrograma para visualizar la distribución de los datos en función de los clústeres obtenidos.

dend_modelo <- as.dendrogram(hc_model)
plot(dend_modelo, ylab = "Similarity")

Hasta ahora, hemos obtenido la jerarquía de los datos, pero lo que realmente nos interesa es la clasificación de los datos en función de los clústeres. Cortaremos el dendrograma en un punto que nos interese para obtener los clústeres. En este caso, y a modo de prueba, hemos decidido cortar el dendrograma en 90 para obtener una visualización del dendograma cortado.

cut <- 0.9

dend_modelo %>%
  color_branches(h = cut) %>%
  color_labels(h = cut) %>%
  plot(ylab = "Similarity")

Para obtener el número óptimo de clúster, haremos uso de la medida interna de bondad silhouette. Para ello, utilizaremos la función fviz_nbclust de factoextra al igual que con k-means.

fviz_nbclust(smaller_df, FUNcluster = hcut, method = "silhouette")

Comprobamos que en este caso, el número óptimo de clústeres podría ser 2 o 3, ya que el valor de silhouette es muy similar para ambos casos. En este caso, hemos decidido utilizar 2 clústeres para poder comparar posteriormente los resultados con los obtenidos con el algoritmo de k-means.

Para generar el modelo de clustering jerárquico, utilizaremos la función cutree de R. Esta función nos permite generar el modelo de clustering jerárquico en función del número de clústeres que queramos obtener.

Calculamos la agrupación del modelo en función del número de clústeres que hemos decidido utilizar, y calculamos el valor promedio de las variables para cada clústergenerado.

jq_cluster <- cutree(hc_model, k = 2)

grouped_mushroom <- smaller_df %>%
  mutate(cluster = jq_cluster) %>%
  group_by(cluster) %>%
  summarise_all(mean)
grouped_mushroom

Visualizamos la agrupación de los datos en función de los clústeres obtenidos a partir del modelo de clustering jerárquico.

jq_clúster<- factor(jq_cluster)

plot_ly(smaller_df,
  x = ~cap.diameter, y = ~stem.height,
  z = ~stem.width,
  color = ~jq_cluster
) %>%
  add_markers(size = 1.5)

Con el objetivo de comparar los resultados obtenidos en los dos algoritmos, vamos a calcular el rendimiento de cada uno de ellos, haciendo uso del accuracy como medida de bondad externa.

En primer lugar, calculamos el accuracy del modelo de k-means. Supondremos que la clase 1 es la clase “e” y la clase 2 es la clase “p”. Para ello, obtenemos las clases reales y las clases predichas, y calculamos el accuracy.

Primero necesitamos volver a obtener el dataset reducido para poder tener las clases reales.

smaller_df <- mushroom[split$Resample1, ]
real_classes <- ifelse(smaller_df$class == "e", 1, 2)
predicted_classes <- km_sm_model$cluster
predicted_classes <- as.numeric(predicted_classes)
accuracy <- sum(real_classes == predicted_classes) / length(real_classes)
print(accuracy)
[1] 0.700491

Hacemos lo mismo con el modelo de clustering jerárquico, pero en este caso, supondremos que la clase 1 es la clase “p” y la clase 2 es la clase “e”.

real_classes <- ifelse(smaller_df$class == "e", 2, 1)
predicted_classes <- as.numeric(jq_cluster)
accuracy <- sum(real_classes == predicted_classes) / length(real_classes)
print(accuracy)
[1] 0.9885434

Tras comparar los resultados obtenidos en los dos algoritmos, podemos afirmar que el modelo de clustering jerárquico ha obtenido un accuracy mayor para este dataset, obteniendo un accuracy del 98% frente al 70% obtenido por el modelo de k-means.

Pese a obtener un accuracy mayor con el modelo de clustering jerárquico, hay que tener en cuenta que el objetivo de aprendizaje no supervisado es la agrupación de los datos en función de sus características, y no la predicción de una variable objetivo. Además, el accuracy obtenido con el modelo de clustering jerárquico es muy alto, lo que puede deberse a que el dataset utilizado es muy reducido y no presenta mucha variabilidad entre las clases.

LS0tCnRpdGxlOiAiTXVzaHJvb20gRGF0YSBBbmFseXNpcyIKb3V0cHV0OgogIGh0bWxfZG9jdW1lbnQ6CiAgICBkZl9wcmludDogcGFnZWQKICBwZGZfZG9jdW1lbnQ6IGRlZmF1bHQKICBodG1sX25vdGVib29rOiBkZWZhdWx0Ci0tLQo8c3R5bGU+CmJvZHkgewp0ZXh0LWFsaWduOiBqdXN0aWZ5fQo8L3N0eWxlPgoKIyDDjW5kaWNlICAgCjEuIFtJbnRyb2R1Y2Npw7NuXSgjaW50cm9kdWN0aW9uKSBcCjIuIFtDYXJnYXIgbGlicmVyw61hc10oI2xpYnJhcmllcykgXAozLiBbQ2FyZ2FyIGRhdG9zXSgjZGF0YSkgXAo0LiBbVmlzdWFsaXphY2nDs24geSBwcmVwcm9jZXNhbWllbnRvXSgjcHJlcHJvY2Vzc2luZykgXAo0LjEgW0ltcHV0YWNpw7NuIGRlIHZhbG9yZXMgbnVsb3NdKCNpbXB1dGF0aW9uKSBcCjQuMiBbQ29kaWZpY2FjacOzbiBkZSB2YXJpYWJsZXMgY2F0ZWfDs3JpY2FzXSgjZW5jb2RpbmcpIFwKNC4zIFtFc2NhbGFkbyBkZSB2YXJpYWJsZXMgbnVtw6lyaWNhc10oI3NjYWxpbmcpCjUuIFtBbsOhbGlzaXMgZGUgZGF0b3NdKCNhbmFseXNpcykgXAo1LjEgW0Fuw6FsaXNpcyBhcHJlbmRpemFqZSBzdXBlcnZpc2Fkb10oI3N1cGVydmlzZWQpIFwKNS4yIFtBbsOhbGlzaXMgYXByZW5kaXphamUgbm8gc3VwZXJ2aXNhZG9dKCN1bnN1cGVydmlzZWQpIFwKNS4yLjEgW0stbWVhbnNdKCNrbWVhbnMpIFwKNS4yLjIgW0NsdXN0ZXJpbmcgamVyw6FycXVpY29dKCNoaWVyYXJjaGljYWwpIFwKClxwYWdlYnJlYWsKCiMgSW50cm9kdWNjacOzbjxhIG5hbWU9ImludHJvZHVjdGlvbiI+PC9hPgoKRXN0ZSB0cmFiYWpvIHNlIGNlbnRyYSBlbiBlbCBhbsOhbGlzaXMgZGUgdW4gY29uanVudG8gZGUgZGF0b3MgcXVlIGNvbnRpZW5lIGluZm9ybWFjacOzbiBzb2JyZSA2MTA2OSBjaGFtcGnDsW9uZXMgZGlmZXJlbnRlcy4gTG9zIGRhdG9zIGhhbiBzaWRvIG9idGVuaWRvcyBkZSBLYWdnbGUgeSBjYWRhIHVuYSBkZSBsYXMgaW5zdGFuY2lhcyBpbmNsdXllIDIxIHZhcmlhYmxlcyBxdWUgZGVzY3JpYmVuIGRpZmVyZW50ZXMgYXNwZWN0b3MgZGUgbG9zIGNoYW1wacOxb25lcywgY29tbyBzdSBmb3JtYSwgdGFtYcOxbyB5IGjDoWJpdGF0LiBFbiBlc3RhcyB2YXJpYWJsZXMgc2UgZW5jdWVudHJhIGlubGN1aWRhIGxhIGNsYXNlIGRlIGNhZGEgY2hhbXBpw7HDs24sIGluZGljYW5kbyBzaSBlcyB2ZW5lbm9zbyBvIGNvbWVzdGlibGUuCgpBbnRlcyBkZSBjb21lbnphciBlbCBhbsOhbGlzaXMsIGVzIG5lY2VzYXJpbyByZWFsaXphciB1bmEgZmFzZSBkZSBwcmVwcm9jZXNhbWllbnRvIGRlIGxvcyBkYXRvcy4gRXN0YSBmYXNlIHRpZW5lIGNvbW8gb2JqZXRpdm8gcHJlcGFyYXIgbG9zIGRhdG9zIHBhcmEgc3UgYW7DoWxpc2lzIHkgb2J0ZW5lciBtYXlvciBjb25vY2ltaWVudG8gc29icmUgZXN0b3MuIER1cmFudGUgZXN0YSBmYXNlLCByZWFsaXphcmVtb3MgdGFyZWFzIGNvbW8gdmlzdWFsaXphciBsb3MgZGF0b3MsIGRldGVjdGFyIHkgdHJhdGFyIHZhbG9yZXMgZmFsdGFudGVzLCBvIG5vcm1hbGl6YXIgbG9zIGRhdG9zLgpVbmEgdmV6IHByZXBhcmFkb3MgbG9zIGRhdG9zLCBwcm9jZWRlcmVtb3MgYSByZWFsaXphciBlbCBhbsOhbGlzaXMuIFBhcmEgZWxsbywgdXRpbGl6YXJlbW9zIHRhbnRvIHTDqWNuaWNhcyBkZSBhcHJlbmRpemFqZSBzdXBlcnZpc2FkbyBjb21vIG5vIHN1cGVydmlzYWRvLgoKRWwgYXByZW5kaXphamUgc3VwZXJ2aXNhZG8gaW1wbGljYSBlbnRyZW5hciB1biBtb2RlbG8gY29uIGRhdG9zIGV0aXF1ZXRhZG9zLCBlcyBkZWNpciwgcXVlIHlhIGNvbm9jZW1vcyBsYSBjbGFzZSBkZSBjYWRhIGluc3RhbmNpYS4gVW5hIHZleiBlbnRyZW5hZG8gZWwgbW9kZWxvLCBwb2RlbW9zIHV0aWxpemFybG8gcGFyYSBwcmVkZWNpciBsYSBjbGFzZSBkZSBudWV2YXMgaW5zdGFuY2lhcy4gRW4gZXN0ZSBjYXNvLCB1dGlsaXphcmVtb3MgZGlmZXJlbnRlcyBhbGdvcml0bW9zIGRlIGNsYXNpZmljYWNpw7NuIHBhcmEgZXZhbHVhciBkaWZlcmVudGVzIG1vZGVsb3MuCgpFbCBhcHJlbmRpemFqZSBubyBzdXBlcnZpc2FkbywgcG9yIG90cm8gbGFkbywgbm8gcmVxdWllcmUgZGUgZGF0b3MgZXRpcXVldGFkb3MuIEVuIGVzdGUgY2FzbywgZWwgb2JqZXRpdm8gZXMgYWdydXBhciBsYXMgaW5zdGFuY2lhcyBlbiBkaWZlcmVudGVzIGdydXBvcyBkZSBmb3JtYSBxdWUgbGFzIGluc3RhbmNpYXMgZGUgdW4gbWlzbW8gZ3J1cG8gc2VhbiBzaW1pbGFyZXMgZW50cmUgc8OtIHkgZGlmZXJlbnRlcyBhIGxhcyBkZSBsb3MgZGVtw6FzIGdydXBvcy4gVXRpbGl6YXJlbW9zIGRvcyBhbGdvcml0bW9zIGRlIGNsdXN0ZXJpbmcgcGFyYSBldmFsdWFyIGRpZmVyZW50ZXMgbW9kZWxvcy4gQWRlbcOhcywgY2FiZSBkZXN0YWNhciwgcXVlIGVuIG51ZXN0cm8gY2FzbyBjb250YW1vcyBjb24gdW4gY29ub2NpbWllbnRvIGEgcHJpb3JpIGRlIGxvcyBkYXRvcywgeWEgcXVlIGNvbm9jZW1vcyBsYSBjbGFzZSBkZSBjYWRhIGluc3RhbmNpYS4gUG9yIHRhbnRvLCBwb2RlbW9zIHV0aWxpemFyIGVzdGUgY29ub2NpbWllbnRvIHBhcmEgZXZhbHVhciBsb3MgbW9kZWxvcyBkZSBjbHVzdGVyaW5nLgoKQSBjb250aW51YWNpw7NuIHNlIGRldGFsbGEgbG9zIHBvc2libGVzIHZhbG9yZXMgcXVlIHB1ZWRlbiB0b21hciBsYXMgdmFyaWFibGVzIGRlbCBkYXRhc2V0OgoKKioxLiBjbGFzczoqKiBlZGlibGU9ZSwgcG9pc29ub3VzPXAgXAoqKjIuIGNhcC1kaWFtZXRlcjoqKiBmbG9hdCBudW1iZXIgaW4gY20gXAoqKjMuIGNhcC1zaGFwZToqKiBiZWxsPWIsIGNvbmljYWw9YywgY29udmV4PXgsIGZsYXQ9Ziwga25vYmJlZD1rLCBzdW5rZW49cyBcCioqNC4gY2FwLXN1cmZhY2U6KiogZmlicm91cz1mLCBncm9vdmVzPWcsIHNjYWx5PXksIHNtb290aD1zIFwKKio1LiBjYXAtY29sb3I6KiogYnJvd249biwgYnVmZj1iLCBjaW5uYW1vbj1jLCBncmF5PWcsIGdyZWVuPXIsIHBpbms9cCwgcHVycGxlPXUsIHJlZD1lLCB3aGl0ZT13LCB5ZWxsb3c9eSBcCioqNi4gZG9lcy5icnVpc2Uub3IuYmxlZWQ6KiogYnJ1aXNlcy1vci1ibGVlZGluZz10LG5vPWYgXAoqKjcuIGdpbGwtYXR0YWNobWVudDoqKiBhdHRhY2hlZD1hLCBkZXNjZW5kaW5nPWQsIGZyZWU9Ziwgbm90Y2hlZD1uIFwKKio4LiBnaWxsLXNwYWNpbmc6KiogY2xvc2U9YywgY3Jvd2RlZD13LCBkaXN0YW50PWQgXAoqKjkuIGdpbGwtY29sb3I6KiogYmxhY2s9aywgYnJvd249biwgYnVmZj1iLCBjaG9jb2xhdGU9aCwgZ3JheT1nLCBncmVlbj1yLCBvcmFuZ2U9bywgcGluaz1wLCBwdXJwbGU9dSwgcmVkPWUsIHdoaXRlPXcsIHllbGxvdz15IFwKKioxMC4gc3RlbS1oZWlnaHQ6KiogZmxvYXQgbnVtYmVyIGluIGNtIFwKKioxMS4gc3RlbS13aWR0aDoqKiBmbG9hdCBudW1iZXIgaW4gbW0gIFwKKioxMi4gc3RlbS1yb290OioqIGJ1bGJvdXM9Yiwgc3dvbGxlbj1zLCBjbHViPWMsIGN1cD11LCBlcXVhbD1lLCByaGl6b21vcnBocz16LCByb290ZWQ9ciBcCioqMTMuIHN0ZW0tc3VyZmFjZToqKiBzZWUgY2FwLXN1cmZhY2UgKyBub25lPWYgXAoqKjE0LiBzdGVtLWNvbG9yOioqIHNlZSBjYXAtY29sb3IgKyBub25lPWYgXAoqKjE1LiB2ZWlsLXR5cGU6KiogcGFydGlhbD1wLCB1bml2ZXJzYWw9dSBcCioqMTYuIHZlaWwtY29sb3I6Kiogc2VlIGNhcC1jb2xvciArIG5vbmU9ZiBcCioqMTcuIGhhcy1yaW5nOioqIHJpbmc9dCwgbm9uZT1mIFwKKioxOC4gcmluZy10eXBlOioqIGNvYndlYmJ5PWMsIGV2YW5lc2NlbnQ9ZSwgZmxhcmluZz1yLCBncm9vdmVkPWcsIGxhcmdlPWwsIHBlbmRhbnQ9cCwgc2hlYXRoaW5nPXMsIHpvbmU9eiwgc2NhbHk9eSwgbW92YWJsZT1tLCBub25lPWYsIHVua25vd249PyBcCioqMTkuIHNwb3JlLXByaW50LWNvbG9yOioqIHNlZSBjYXAgY29sb3IgXAoqKjIwLiBoYWJpdGF0OioqIGdyYXNzZXM9ZywgbGVhdmVzPWwsIG1lYWRvd3M9bSwgcGF0aHM9cCwgaGVhdGhzPWgsIHVyYmFuPXUsIHdhc3RlPXcsIHdvb2RzPWQgXAoqKjIxLiBzZWFzb246Kiogc3ByaW5nPXMsIHN1bW1lcj11LCBhdXR1bW49YSwgd2ludGVyPXcgXAoKIyBBdXRvcmVzPGEgbmFtZT0iYXV0aG9ycyI+PC9hPgpFc3RlIHRyYWJham8gaGEgc2lkbyByZWFsaXphZG8gcG9yOgoKKiBBbmEgRMOtYXogTXXDsW96CgoqIE1hcsOtYSBJc2FiZWwgUmFtb3MgQmxhbmNvCgoqIERleWFuIFJvc2Vub3YgU3RhbmNoZXYKCiogSmF2aWVyIFZpbGFyacOxbyBNYXlvCgoKVG9kb3MgbG9zIGludGVncmFudGVzIGRlbCBncnVwbyBoYW4gcmVhbGl6YWRvIGRlIGZvcm1hIGNvbmp1bnRhIGxhIHZpc3VhbGl6YWNpw7NuIHkgZWwgcHJlcHJvY2VzYW1pZW50byBkZSBsb3MgZGF0b3MuIEFuYSB5IERleWFuIHNlIGhhIGVuY2FyZ2FkbyBkZSByZWFsaXphciBlbCBhcHJlbmRpemFqZSBzdXBlcnZpc2FkbywgbWllbnRyYXMgcXVlIEphdmllciB5IE1hcsOtYSBJc2FiZWwgc2UgaGFuIGVuY2FyZ2FkbyBkZSByZWFsaXphciBlbCBhcHJlbmRpemFqZSBubyBzdXBlcnZpc2Fkby4KUG9yIG90cm8gbGFkbywgdG9kb3MgbG9zIGNvbXBvbmVudGVzIGhhbiBpbnZlc3RpZ2FkbyBhY2VyY2EgZGVsIGNsdXN0ZXJpbmcgdXRpbGl6YWRvIGVuIEJpZ01MLgoKCiMgQ2FyZ2FyIGxpYnJlcsOtYXM8YSBuYW1lPSJsaWJyYXJpZXMiPjwvYT4KCkVuIGxvcyBzaWd1aWVudGVzIGZyYWdtZW50b3Mgc2UgaW5jbHV5ZSBlbCBjw7NkaWdvIG5lY2VzYXJpbyBwYXJhIGluc3RhbGFyIHkgY2FyZ2FyIGxhcyBsaWJyZXLDrWFzIG5lY2VzYXJpYXMgcGFyYSBlbCBhbsOhbGlzaXMgZGUgbG9zIGRhdG9zIHJlYWxpemFkby4KYGBge3J9CiNpbnN0YWxsLnBhY2thZ2VzKCJjYXJldCIpCiNpbnN0YWxsLnBhY2thZ2VzKCJ0aWR5dmVyc2UiKQojaW5zdGFsbC5wYWNrYWdlcygicGxvdGx5IikKI2luc3RhbGwucGFja2FnZXMoImRwbHlyIikKI2luc3RhbGwucGFja2FnZXMoImZhY3RvZXh0cmEiKQojaW5zdGFsbC5wYWNrYWdlcygiZGVuZGV4dGVuZCIpCmBgYApgYGB7cn0KbGlicmFyeShjYXJldCkKbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkocGxvdGx5KQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KGNsdXN0ZXIpCmxpYnJhcnkoZmFjdG9leHRyYSkKbGlicmFyeShkZW5kZXh0ZW5kKQpgYGAKCiMgQ2FyZ2FyIGRhdG9zPGEgbmFtZT0iZGF0YSI+PC9hPgoKRW4gcHJpbWVyIGx1Z2FyLCByZWFsaXphbW9zIGxhIGxlY3R1cmEgZGVsIGZpY2hlcm8gY3N2LCBzZXBhcmFuZG8gbGFzIGNvbHVtbmFzIHBvciAiOyIgeSBtb3N0cmFtb3MgbGFzIHByaW1lcmFzIDYgZmlsYXMgZGVsIGZpY2hlcm8uIEVzdGUgY8OzZGlnbyBub3MgcGVybWl0aXLDoSBhY2NlZGVyIGEgbG9zIGRhdG9zIGRlIGNoYW1wacOxb25lcyB5IHRyYWJhamFyIGNvbiBlbGxvcyBlbiBudWVzdHJvIGPDs2RpZ28gUi4KYGBge3J9Cm11c2hyb29tIDwtIHJlYWQuY3N2KCIuL2RhdGEvZGF0YS5jc3YiLCBzZXAgPSAiOyIpCmhlYWQobXVzaHJvb20pCmBgYAoKRWNoYW1vcyB1biB2aXN0YXpvIGEgbGFzIGNhcmFjdGVyw61zdGljYXMgZGUgbG9zIGF0cmlidXRvcyBkZWwgZGF0YXNldC4gRW4gZWwgY2FzbyBkZSBsYXMgdmFyaWFibGVzIG51bcOpcmljYXMsIHNlIHB1ZWRlIG9ic2VydmFyIHZhbG9yZXMgY29tbyBlbCBtw61uaW1vLCBtw6F4aW1vLCBtZWRpYSwgZGVzdmlhY2nDs24gZXN0w6FuZGFyLCBldGMuIFBvciBvdHJvIGxhZG8sIGVuIGVsIGNhc28gZGUgbGFzIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMgbm8gb2J0ZW5lbW9zIGluZm9ybWFjacOzbiByZWxldmFudGUuCmBgYHtyfQpzdW1tYXJ5KG11c2hyb29tKQpgYGAKCiMgVmlzdWFsaXphY2nDs24geSBwcmVwcm9jZXNhbWllbnRvPGEgbmFtZT0icHJlcHJvY2Vzc2luZyI+PC9hPgoKRW4gcHJpbWVyIGx1Z2FyLCB2YW1vcyBhIGNvbXByb2JhciBzaSBleGlzdGVuIHZhbG9yZXMgbnVsb3MgZW4gZWwgZGF0YXNldC4gUGFyYSBlbGxvLCB1dGlsaXphcmVtb3MgbGEgZnVuY2nDs24gY29sU3Vtcyhpcy5uYShtdXNocm9vbSkpLCBxdWUgbm9zIGRldm9sdmVyw6EgbGEgc3VtYSBkZSB2YWxvcmVzIG51bG9zIGRlIGNhZGEgdmFyaWFibGUuCmBgYHtyfQpjb2xTdW1zKGlzLm5hKG11c2hyb29tKSkKYGBgCgpTZWfDum4gZWwgcmVzdWx0YWRvIG9idGVuaWRvIGFudGVyaW9ybWVudGUsIG5vIGV4aXN0ZW4gdmFsb3JlcyBudWxvcyBlbiBlbCBkYXRhc2V0LiBTaW4gZW1iYXJnbywgc2kgb2JzZXJ2YW1vcyBlbCBkYXRhc2V0LCBwb2RlbW9zIG9ic2VydmFyIHF1ZSBleGlzdGVuIHZhbG9yZXMgdmFjw61vcy4gUGFyYSBwb2RlciB0cmFiYWphciBjb24gZWxsb3MsIHkgcXVlIG5vIG5vcyBkZSBwcm9ibGVtYXMgYSBsYSBob3JhIGRlIHJlYWxpemFyIGVsIHByZXByb2Nlc2FtaWVudG8sIHN1c3RpdHVpcmVtb3MgZGljaG9zIHZhbG9yZXMgdmFjw61vcyBwb3IgTkEuCmBgYHtyfQptdXNocm9vbVttdXNocm9vbSA9PSAiIl0gPC0gTkEKYGBgCgpDb21wcm9iYW1vcyBxdWUgc2UgaGFuIHN1c3RpdHVpZG8gY29ycmVjdGFtZW50ZSBsb3MgdmFsb3JlcyB2YWPDrW9zIHBvciBOQSwgcHVkaWVuZG8gY29tcHJvYmFyIGxhIGNhbnRpZGFkIGRlIHZhbG9yZXMgbnVsb3MgcXVlIGhheSBlbiBjYWRhIHZhcmlhYmxlLgpFc3RhIGluZm9ybWFjacOzbiBub3MgYXl1ZGFyw6EgYSBkZWNpZGlyIHNpIHNlcsOhIGNvbnZlbmllbnRlIGVsaW1pbmFyIGRpY2hhcyB2YXJpYWJsZXMgbyBuby4KYGBge3J9CmNvbFN1bXMoaXMubmEobXVzaHJvb20pKQpgYGAKUG9kZW1vcyBvYnNlcnZhciBxdWUgZXhpc3RlbiA1IHZhcmlhYmxlcyBkb25kZSBtw6FzIGRlbCA1MCUgZGUgbG9zIHZhbG9yZXMgc29uIG51bG9zLiBFc3RhcyBzb246IHN0ZW0uc3VyZmFjZSwgdmVpbC5jb2xvciwgc3BvcmUucHJpbnQuY29sb3IsIHN0ZW0ucm9vdCwgdmVpbC50eXBlLgoKRW4gZXN0ZSBjYXNvLCBkZWNpZGltb3MgZWxpbWluYXIgYXF1ZWxsYXMgdmFyaWFibGVzIHF1ZSBjdWVudGFuIGNvbiBtw6FzIGRlbCA1MCUgZGUgc3VzIHZhbG9yZXMgZmFsdGFudGVzLiBMYSByYXrDs24gZGUgZXN0YSBkZWNpc2nDs24gZXMgcXVlLCBzaSBkZWNpZGltb3MgaW1wdXRhciBsb3MgdmFsb3JlcyBmYWx0YW50ZXMsIGVzdGFyw61hbW9zIGludmVudGFuZG8gZGVtYXNpYWRvcyBkYXRvcy4gSW1wdXRhciB2YWxvcmVzIGZhbHRhbnRlcyBzaWduaWZpY2EgcmVlbXBsYXphciBsb3MgdmFsb3JlcyBmYWx0YW50ZXMgY29uIGFsZ8O6biB2YWxvciBxdWUgZXN0aW1lbW9zIGFkZWN1YWRvLiBTaW4gZW1iYXJnbywgc2kgdW5hIHZhcmlhYmxlIHRpZW5lIG3DoXMgZGVsIDUwJSBkZSBzdXMgdmFsb3JlcyBmYWx0YW50ZXMsIHNpZ25pZmljYSBxdWUgZXN0YXLDrWFtb3MgcmVlbXBsYXphbmRvIG3DoXMgZGUgbGEgbWl0YWQgZGUgbG9zIHZhbG9yZXMgZGUgZXNhIHZhcmlhYmxlLiBFc3RvIG5vcyBsbGV2YXLDrWEgYSB0ZW5lciB1biBjb25qdW50byBkZSBkYXRvcyBjb24gZGVtYXNpYWRvcyB2YWxvcmVzIGludmVudGFkb3MsIGxvIHF1ZSBwb2Ryw61hIGFmZWN0YXIgbGEgcHJlY2lzacOzbiBkZSBudWVzdHJvIGFuw6FsaXNpcy4KUGFyYSBlbGxvLCBlbiBwcmltZXIgbHVnYXIsIG9idGVuZHJlbW9zIGVsIG5vbWJyZSBkZSBsYXMgY29sdW1uYXMgcXVlIHF1ZXJlbW9zIGVsaW1pbmFyLgoKYGBge3J9Cm5hY29scyA8LSBjb2xuYW1lcyhtdXNocm9vbSlbY29sU3Vtcyhpcy5uYShtdXNocm9vbSkpID4gbnJvdyhtdXNocm9vbSkgLyAyXQpwcmludChuYWNvbHMpCmBgYAoKQSBjb250aW51YWNpw7NuLCBlbGltaW5hcmVtb3MgZGljaGFzIGNvbHVtbmFzIGRlbCBkYXRhc2V0LgpgYGB7cn0KbXVzaHJvb20gPC0gbXVzaHJvb21bLCAhbmFtZXMobXVzaHJvb20pICVpbiUgbmFjb2xzXQpgYGAKCkNvbXByb2JhbW9zIHF1ZSBzZSBoYW4gZWxpbWluYWRvIGNvcnJlY3RhbWVudGUgbGFzIGNvbHVtbmFzLgpgYGB7cn0KcHJpbnQoY29sbmFtZXMobXVzaHJvb20pKQpgYGAKClBhcmEgcG9kZXIgYW5hbGl6YXIgZGUgZm9ybWEgZXNwZWPDrWZpY2EgY2FkYSB2YXJpYWJsZSwgc2VwYXJhbW9zIGxhcyB2YXJpYWJsZXMgbnVtw6lyaWNhcyBkZSBsYXMgY2F0ZWfDs3JpY2FzLgpgYGB7cn0KY29sc25hbWVzIDwtIGNvbG5hbWVzKG11c2hyb29tKQpudW1lcmljYWxfZmVhdHVyZXMgPC0gYygiY2FwLmRpYW1ldGVyIiwgInN0ZW0uaGVpZ2h0IiwgInN0ZW0ud2lkdGgiKQpjYXRlZ29yaWNhbF9mZWF0dXJlcyA8LSBjb2xzbmFtZXNbIWNvbHNuYW1lcyAlaW4lIG51bWVyaWNhbF9mZWF0dXJlc10KcHJpbnQoY2F0ZWdvcmljYWxfZmVhdHVyZXMpCnByaW50KG51bWVyaWNhbF9mZWF0dXJlcykKYGBgCgpDb21lbnphbW9zIGFuYWxpemFuZG8gbGFzIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMuIFBhcmEgZWxsbyB2aXN1YWxpemFtb3MgbGEgZGlzdHJpYnVjacOzbiBkZSBsYXMgdmFyaWFibGVzIGNhdGVnw7NyaWNhcyBhIHRyYXbDqXMgZGUgaGlzdG9ncmFtYXMuIE9ic2VydmFyZW1vcyBsb3MgcG9zaWJsZXMgdmFsb3JlcyBkZSBjYWRhIHZhcmlhYmxlIGNhdGVnw7NyaWNhIGp1bnRvIGNvbiBzdSBmcmVjdWVuY2lhIGRlIGFwYXJpY2nDs24gZW4gZWwgZGF0YXNldC4KYGBge3J9CmZvciAoaSBpbiBjYXRlZ29yaWNhbF9mZWF0dXJlcykgewogIHByaW50KGdncGxvdChtdXNocm9vbSwgYWVzX3N0cmluZyh4ID0gaSkpICsKICAgIGdlb21fYmFyKGZpbGwgPSAiIzdmZDZkOSIpICsKICAgIGdlb21fdGV4dChzdGF0ID0gImNvdW50IiwgYWVzKGxhYmVsID0gc2NhbGVzOjpwZXJjZW50KC4uY291bnQuLiAvIG5yb3cobXVzaHJvb20pKSwgdmp1c3QgPSAtMC4yNSkpICsKICAgIGxhYnMoeCA9IGksIHkgPSAiUGVyY2VudGFnZSIpICsKICAgIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gOTAsIGhqdXN0ID0gMSkpKQp9CmBgYAoKVHJhcyB2aXN1YWxpemFyIGxvcyBoaXN0b2dyYW1hcyBkZSBjYWRhIHZhcmlhYmxlLCBvYnRlbmVtb3MgbGFzIHNpZ3VpZW50ZXMgY29uY2x1c2lvbmVzOgoKKiBMYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSAiY2xhc3MiIGVzdMOhIGJhbGFuY2VhZGEsIGVzIGRlY2lyLCBsYSBmcmVjdWVuY2lhIGRlIGFwYXJpY2nDs24gZGUgImUiIChlZGlibGUpIHkgInAiIChwb2lzb25vdXMpIGVzIHNpbWlsYXIsIGVuIHVuYSBwcm9wb3JjacOzbiBkZSA0NCw1JSB5IDU1LDUlIHJlc3BlY3RpdmFtZW50ZS4gUG9yIGxvIHRhbnRvLCBubyBlcyBuZWNlc2FyaW8gcmVhbGl6YXIgdW4gYmFsYW5jZW8gZGUgbGEgdmFyaWFibGUgZGVwZW5kaWVudGUsIHBlcm1pdGllbmRvIGFwbGljYXIgbGEgbWVkaWRhIGRlIGV2YWx1YWNpw7NuICJhY2N1cmFjeSIgcGFyYSBldmFsdWFyIGVsIG1vZGVsby4KKiBTaWd1ZW4gZXhpc3RpZW5kbyB2YWxvcmVzIE5BIGVuIGFsZ3VuYXMgdmFyaWFibGVzLCBwZXJvIGVzdG9zIHNlcsOhbiBpbXB1dGFkb3MgcG9zdGVyaW9ybWVudGUgY29uIGxhIG1vZGEgZGUgY2FkYSB2YXJpYWJsZS4KKiBMYSB2YXJpYWJsZSByaW5nLXR5cGUgdGllbmUgOCBwb3NpYmxlcyB2YWxvcmVzLCBwZXJvIGVuIGVsIDgwJSBkZSBsb3MgY2Fzb3MsIGVsIHZhbG9yIGVzICJmIiwgcG9yIGxvIHF1ZSBxdWl6w6FzIHNlIHBvZHLDrWEgZWxpbWluYXIgZXN0YSB2YXJpYWJsZSBlbiB1biBmdXR1cm8sIHlhIHF1ZSBwYXJlY2UgcXVlIGFwb3J0YSBpbmZvcm1hY2nDs24gaXJyZWxldmFudGUuIE3DoXMgYWRlbGFudGUgbGEgYW5hbGl6YXJlbW9zIGVuIHByb2Z1bmRpZGFkLgoKQSBjb250aW51YWNpw7NuLCBzZWd1aXJlbW9zIGNvbiBlbCBhbsOhbGlzaXMgZGUgbGFzIHZhcmlhYmxlcyBudW3DqXJpY2FzLCBkb25kZSB2aXN1YWxpemFyZW1vcyBzdSBkaXN0cmlidWNpw7NuIHJlc3BlY3RvIGEgbGEgY2xhc2UgYSB0cmF2w6lzIGRlIHVuYSBncsOhZmljYSBnZW5lcmFkYSBjb24gImZlYXR1cmVQbG90Ii4gRXN0YSByZXF1aWVyZSBxdWUgbGEgdmFyaWFibGUgb2JqZXRpdm8gc2VhIGRlIHRpcG8gZmFjdG9yLCBwb3IgbG8gcXVlIGhhY2Vtb3MgbGEgY29udmVyc2nDs24uCmBgYHtyfQptdXNocm9vbSRjbGFzcyA8LSBhcy5mYWN0b3IobXVzaHJvb20kY2xhc3MpCmZlYXR1cmVQbG90KHggPSBtdXNocm9vbVssIG51bWVyaWNhbF9mZWF0dXJlc10sIHkgPSBtdXNocm9vbSRjbGFzcywgcGxvdCA9ICJzdHJpcCIpCmBgYApDb24gbGEgZ3LDoWZpY2EgYW50ZXJpb3IsIHNlIHB1ZWRlIG9ic2VydmFyIHF1ZSBwYXJhIHZhbG9yZXMgYWx0b3MgZGUgY2FwLmRpYW1ldGVyLCBzdGVtLmhlaWdodCB5IHN0ZW0ud2lkdGgsIGxhIHByb2JhYmlsaWRhZCBkZSBxdWUgbGEgY2xhc2Ugc2VhICJlIiBlcyBtYXlvciBxdWUgbGEgZGUgInAiLiBQb3IgbG8gdGFudG8sIHBvZGVtb3MgZGVkdWNpciBxdWUgc2kgdW5hIHNldGEgZXMgZGUgdGFtYcOxbyBncmFuZGUsIHN1IHByb2JhYmlsaWRhZCBkZSBzZXIgY29tZXN0aWJsZSBlcyBiYXN0YW50ZSBtYXlvci4KCgpBbnRlcmlvcm1lbnRlIGhlbW9zIGNvbWVudGFkbyBxdWUgbGEgdmFyaWFibGUgInJpbmcudHlwZSIgdGllbmUgOCBwb3NpYmxlcyB2YWxvcmVzLCBwZXJvIGVuIGNhc2kgdW4gODAlIGRlIGxvcyBjYXNvcywgZWwgdmFsb3IgZXMgImYiLiBQYXJhIGV2YWx1YXIgc3UgcG9zaWJsZSBlbGltaW5hY2nDs24gbyBsYSBkZSBhbGd1bmEgb3RyYSB2YXJpYWJsZSwgc2UgcHVlZGUgdXRpbGl6YXIgbGEgZnVuY2nDs24gIm5lYXJaZXJvVmFyIi4gRXN0YSBmdW5jacOzbiBkZXZ1ZWx2ZSB1biB2ZWN0b3IgY29uIGxvcyDDrW5kaWNlcyBkZSBsYXMgdmFyaWFibGVzIHF1ZSB0aWVuZW4gdW5hIHZhcmlhbnphIGNlcmNhbmEgYSAwLgpgYGB7cn0KbmVhcl96ZXJvX2NvbCA8LSBuZWFyWmVyb1ZhcihtdXNocm9vbSwgc2F2ZU1ldHJpY3MgPSBGQUxTRSkKY29sbmFtZXMobXVzaHJvb20pW2MobmVhcl96ZXJvX2NvbCldCmBgYAoKQ29tbyBzdXBvbsOtYW1vcywgbGEgdmFyaWFibGUgInJpbmcudHlwZSIgZXMgbGEgcXVlIHRpZW5lIHVuYSB2YXJpYW56YSBjZXJjYW5hIGEgMCwgeSBwb3IgdGFudG8gcG9kcsOtYSBzZXIgZWxpbWluYWRhLiBBIGNvbnRpbnVhY2nDs24sIHZpc3VhbGl6YXJlbW9zIGxhIGNvcnJlbGFjacOzbiBleGlzdGVudGUgY29uIGxhIHZhcmlhYmxlIGRlcGVuZGllbnRlICJjbGFzcyIgcGFyYSB0b21hciB1bmEgZGVjaXNpw7NuLgpgYGB7cn0KcHJpbnQoZ2dwbG90KG11c2hyb29tLCBhZXNfc3RyaW5nKHggPSAicmluZy50eXBlIikpICsKICBnZW9tX2JhcihhZXMoZmlsbCA9IGNsYXNzKSkpCmBgYAoKVHJhcyB2aXN1YWxpemFyIGxhIGdyw6FmaWNhIGFudGVyaW9yLCBzZSBwdWVkZSBvYnNlcnZhciBxdWUgbGEgZGlzdHJpYnVjacOzbiBkZSBsYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSAiY2xhc3MiIGVzIHNpbWlsYXIgZW4gY2FzaSB0b2RvcyBsb3MgdmFsb3JlcyBkZSAicmluZy50eXBlIi4gU2luIGVtYmFyZ28sIGVuIGVsIGNhc28gZGUgInJpbmcudHlwZSIgPSAieiIgeSAicmluZy50eXBlIiA9ICJtIiwgbGEgZGlzdHJpYnVjacOzbiBkZSBsYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSAiY2xhc3MiIGVzIGRpZmVyZW50ZS4gUG9yIGxvIHRhbnRvLCBzZSBkZWNpZGUgZmluYWxlbW50ZSBtYW50ZW5lciBsYSB2YXJpYWJsZSAicmluZy50eXBlIi4KCiMjIEltcHV0YWNpw7NuIGRlIHZhbG9yZXMgbnVsb3M8YSBuYW1lPSJpbXB1dGF0aW9uIj48L2E+CgpDb21vIHNlIGhhIGNvbWVudGFkbyBhbnRlcmlvcm1lbnRlLCBsb3MgdmFsb3JlcyBudWxvcyBkZSBsYXMgdmFyaWFibGVzIGNhdGVnw7NyaWNhcyBzZSBpbXB1dGFyw6FuIGEgdHJhdsOpcyBkZSBsYSBtb2RhIGRlIGNhZGEgdmFyaWFibGUuIApFc3RlIHByb2Nlc28gbG8gaGFjZW1vcyBkZSBmb3JtYSBtYW51YWwsIHlhIHF1ZSBsYSBmdW5jacOzbiBwcmVQcm9jZXNzKCkgZGUgbGEgbGlicmVyw61hIGNhcmV0IG5vIHBlcm1pdGUgaW1wdXRhciB2YWxvcmVzIG51bG9zIGRlIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMuCmBgYHtyfQpmb3IgKGkgaW4gY2F0ZWdvcmljYWxfZmVhdHVyZXMpIHsKICBtdXNocm9vbVssIGldW2lzLm5hKG11c2hyb29tWywgaV0pXSA8LSBuYW1lcyh3aGljaC5tYXgodGFibGUobXVzaHJvb21bLCBpXSkpKQp9CmBgYAoKQ29tcHJvYmFtb3MgcXVlIHlhIG5vIGV4aXN0ZW4gdmFsb3JlcyBudWxvcyBlbiBsYXMgdmFyaWFibGVzIGNhdGVnw7NyaWNhcy4KYGBge3J9CmNvbFN1bXMoaXMubmEobXVzaHJvb20pKQpgYGAKCiMjIEVzY2FsYWRvIGRlIHZhcmlhYmxlcyBudW3DqXJpY2FzPGEgbmFtZT0ic2NhbGluZyI+PC9hPgoKQ29uIGVsIG9iamV0aXZvIGRlIHF1ZSB0b2RhcyBsYXMgdmFyaWFibGVzIHRlbmdhbiBsYSBtaXNtYSBlc2NhbGEgeSBldml0YXIgcXVlIHVuYSB2YXJpYWJsZSB0ZW5nYSBtw6FzIHBlc28gcXVlIG90cmEsIHNlIGVzY2FsYXLDoW4gbGFzIHZhcmlhYmxlcyBudW3DqXJpY2FzLgpQYXJhIHJlYWxpemFyIGVzdGUgZXNjYWxhZG8sIHNlIHV0aWxpemFyw6EgbGEgZnVuY2nDs24gcHJlUHJvY2VzcygpIGRlIGxhIGxpYnJlcsOtYSBjYXJldC4gRXN0YSBmdW5jacOzbiBkZXZ1ZWx2ZSB1biBvYmpldG8gZGUgdGlwbyAicHJlUHJvY2VzcyIgcXVlIGNvbnRpZW5lIGxhIGluZm9ybWFjacOzbiBuZWNlc2FyaWEgcGFyYSBlc2NhbGFyIGxhcyB2YXJpYWJsZXMgbnVtw6lyaWNhcy4gCkEgY29udGludWFjacOzbiwgc2UgcmVhbGl6YXLDoSBlbCBlc2NhbGFkbyB5IHNlIHN1c3RpdHVpcsOhbiBsYXMgdmFyaWFibGVzIG51bcOpcmljYXMgb3JpZ2luYWxlcyBwb3IgbGFzIGVzY2FsYWRhcy4KYGBge3J9CnJhbmdlX251bWVyaWMgPC0gcHJlUHJvY2VzcyhtdXNocm9vbVssIG51bWVyaWNhbF9mZWF0dXJlc10sIG1ldGhvZCA9IGMoInJhbmdlIikpCm11c2hyb29tWywgbnVtZXJpY2FsX2ZlYXR1cmVzXSA8LSBwcmVkaWN0KHJhbmdlX251bWVyaWMsIG5ld2RhdGEgPSBtdXNocm9vbVssIG51bWVyaWNhbF9mZWF0dXJlc10pCnN0cihtdXNocm9vbSkKYGBgCgoKIyBBbsOhbGlzaXMgZGUgZGF0b3M8YSBuYW1lPSJhbmFseXNpcyI+PC9hPgoKCiMjIEFuw6FsaXNpcyBhcHJlbmRpemFqZSBzdXBlcnZpc2FkbzxhIG5hbWU9InN1cGVydmlzZWQiPjwvYT4KClBhcmEgdHJhYmFqYXIgY29uIGVsIGRhdGFzZXQgbWVkaWFudGUgZWwgYXByZW5kaXphamUgc3VwZXJ2aXNhZG87IGVzIGRlY2lyLCB1dGlsaXphbmRvIGRhdG9zIHF1ZSBzb24gZXRpcXVldGFkb3MgbWVkaWFudGUgbGEgaW50ZXJ2ZW5jacOzbiBkZSB1biBzZXIgaHVtYW5vLCB1dGlsaXphcmVtb3MgZGlmZXJlbnRlcyB0aXBvcyBkZSBjbGFzaWZpY2Fkb3JlcywgYWxndW5vcyBkZSBlbGxvcyB5YSB0cmFiYWphZG9zIGR1cmFudGUgbGFzIGNsYXNlcyBwcsOhY3RpY2FzIGRlIGxhIGFzaWduYXR1cmEgeSBvdHJvcyBxdWUgZXJhbiBkZXNjb25vY2lkb3MgcGFyYSBub3NvdHJvcyB5IHNvYnJlIGxvcyBjdWFsZXMgaGVtb3MgdGVuaWRvIHF1ZSBpbnZlc3RpZ2FyIGFudGVyaW9ybWVudGUgc29icmUgc3UgZnVuY2lvbmFtaWVudG8gZW4gUi4KTG9zIGFsZ29yaXRtb3MgZGUgY2xhc2lmaWNhY2nDs24gcXVlIGhlbW9zIHNlbGVjY2lvbmFkbyB5IGxvcyBjdWFsZXMgdmFtb3MgYSBhcGxpY2FyIHNvbjoKCiAgMS4gUmVncmVzacOzbiBsb2fDrXN0aWNhLgogIAogIDIuIEtOTiBvIGVsIFZlY2lubyBtw6FzIGNlcmNhbm8uCiAgCiAgMy4gw4FyYm9sZXMgZGUgZGVjaXNpw7NuLgogIAogIDQuIFJhbmRvbSBGb3Jlc3QuCiAgCiAgNS4gTVNWIG8gTcOhcXVpbmEgZGUgU29wb3J0ZSBWZWN0b3JpYWwuCiAgClVuYSB2ZXogYXBsaWNhZG9zIGNhZGEgdW5vIGRlIGxvcyBjbGFzaWZpY2Fkb3JlcywgbG9zIGNvbXBhcmFyZW1vcyBlbnRyZSBzw60geSBzZWxlY2Npb25hcmVtb3MgZWwgbyBsb3MgYWxnb3JpdG1vcyBxdWUgbWF5b3IgcHJlY2lzacOzbiBwcm9wb3JjaW9uZW4gc2luIGxsZWdhciBhbCBzb2JyZWFqdXN0ZSwgdHJhdGFuZG8gZGUgYnVzY2FyIHF1ZSBlbCByZXN1bHRhZG8gZmluYWwgc2VhIGdlbmVyYWxpemFkbyBwYXJhIGxvcyBkYXRvcy4KCgpFbiBwcmltZXIgbHVnYXIsIGFudGUgZGUgY29tZW56YXIgYSBhcGxpY2FyIGxvcyBjbGFzaWZpY2Fkb3JlcywgZGl2aWRpcmVtb3MgZWwgZGF0YXNldCBlbiBkb3MgY29uanVudG9zOiBlbCBwcmltZXIgY29uanVudG8gc2Vyw6EgZWwgZGUgZW50cmVuYW1pZW50byBvIHRyYWluaW5nIHkgZWwgc2VndW5kbyBzZXLDoSBlbCBjb25qdW50byBkZSBwcnVlYmEgbyB0ZXN0LiBFbCBjb25qdW50byBkZSBlbnRyZW5hbWllbnRvIGxvIHV0aWxpemFyZW1vcyBwYXJhIGVudHJlbmFyIGxvcyBkaXN0aW50b3MgbW9kZWxvcyB5IGVsIGNvbmp1bnRvIGRlIHBydWViYSBsbyB1dGlsaXphcmVtb3MgcGFyYSBldmFsdWFyIGNhZGEgdW5vIGRlIGVsbG9zIHVuYSB2ZXogb2J0ZW5pZG9zLgoKYGBge3J9CmxpYnJhcnkoY2FUb29scykKc2V0LnNlZWQoMTgpCgpzcGxpdCA8LSBzYW1wbGUuc3BsaXQobXVzaHJvb20kY2xhc3MsIFNwbGl0UmF0aW8gPSAwLjgpCnRyYWluaW5nX3NldCA8LSBzdWJzZXQobXVzaHJvb20sIHNwbGl0ID09IFRSVUUpCnRlc3Rfc2V0IDwtIHN1YnNldChtdXNocm9vbSwgc3BsaXQgPT0gRkFMU0UpCgp0YWJsZSh0cmFpbmluZ19zZXQkY2xhc3MpCnRhYmxlKHRlc3Rfc2V0JGNsYXNzKQpgYGAKCgojIyMgUmVncmVzacOzbiBMb2fDrXN0aWNhPGEgbmFtZT0ibG9naXN0aWMiPjwvYT4KCkxhIHJlZ3Jlc2nDs24gbG9nw61zdGljYSBwZXJtaXRlIHByZWRlY2lyIGVsIHJlc3VsdGFkbyBkZSB1bmEgdmFyaWFibGUgY2F0ZWfDs3JpY2EgZW4gZnVuY2nDs24gZGUgbGFzIHZhcmlhYmxlcyBpbmRlcGVuZGllbnRlcyBvIHByZWRpY3RvcmFzLgpBIGNvbnRpbnVhY2nDs24gc2UgbXVlc3RyYSB1biByZXN1bWVuIGRlbCBjb25qdW50byBkZSBkYXRvcyBkZSBlbnRyZW5hbWllbnRvIGNvbiBlbCBjdWFsIHZhbW9zIGEgdHJhYmFqYXIgdW5hIHZleiBhcGxpY2FkYSBsYSBmdW5jacOzbiBnbG0oKS4KCmBgYHtyfQpybF9jbGFzc2lmZmllciA8LSBnbG0oY2xhc3MgfiAuLCBmYW1pbHkgPSBiaW5vbWlhbCwgZGF0YSA9IHRyYWluaW5nX3NldCkKc3VtbWFyeShybF9jbGFzc2lmZmllcikKYGBgCgpMYSBmdW5jacOzbiBhcGxpY2FkYSwgZ2xtKCksIG9idGllbmUgbG9zIHZhbG9yZXMgcmVzaWR1YWxlcyBkZWwgbW9kZWxvIHkgbG9zIGNvZWZpY2llbnRlcyBkZSBhanVzdGUgcGFyYSBjYWRhIHVuYSBkZSBsYXMgdmFyaWFibGVzIGluZGVwZW5kaWVudGVzLiBBZGVtw6FzLCBzZSBvYnRpZW5lIGVsIHAtdmFsdWUgY29ycmVzcG9uZGllbnRlIHBhcmEgY2FkYSB1bmEgZGUgZWxsYXMuCkVuIGVsIHJlc3VtZW4gbW9zdHJhZG8gbWVkaWFudGUgbGEgZnVuY2nDs24gc3VtbWFyeSgpLCBzZSBwdWVkZW4gb2JzZXJ2YXIgdmFyaWFibGVzIGNvbiBkb3MgbyB0cmVzIGFzdGVyaXNjb3MsIGxhcyBjdWFsZXMgYXBvcnRhbiBiYXN0YW50ZSByZWxldmFuY2lhIGFsIG1vZGVsbyBjb21vIHByZWRpY3RvcmVzOyBzaW4gZW1iYXJnbywgbGFzIHZhcmlhYmxlcyBxdWUgcG9zZWVuIHVuIHNvbG8gYXN0ZXJpc2NvIG8gaW5jbHVzbyBuaW5ndW5vLCBzaWduaWZpY2EgcXVlIGFwZW5hcyBhcG9ydGFuIHJlbGV2YW5jaWEgYSBsb3MgcmVzdWx0YWRvcy4KQSBjb250aW51YWNpw7NuLCBwcm9jZWRlbW9zIGEgcHJlZGVjaXIgbGFzIGNsYXNlcyBkZWwgY29uanVudG8gZGUgZW50cmVuYW1pZW50byB5IGRlIHZhbGlkYWNpw7NuLiBUb21hbmRvIGNvbW8gdW1icmFsICcwLDUnLCBkaXZpZGlyZW1vcyBsb3MgY2hhbXBpw7FvbmVzIGVuIGNvbWVzdGlibGVzIHkgdmVuZW5vc29zLCBkZSBmb3JtYSBxdWUgc2kgbGEgcHJvYmFiaWxpZGFkIHF1ZWRhIHBvciBlbmNpbWEgZGUgZGljaG8gdW1icmFsIHNpZ25pZmljYXLDoSBxdWUgZWwgY2hhbXBpw7HDs24gZXMgY29tZXN0aWJsZSwgeSBkZSBsbyBjb250cmFyaW8sIHNpIGVsIHJlc3VsdGFkbyBzZSBtYW50aWVuZSBwb3IgZGViYWpvLCBzaWduaWZpY2Fyw6EgcXVlIGVsIGNoYW1wacOxw7NuIGVzIHZlbmVub3NvLiBQYXJhIGFjYWJhciwgZm9ybWFyZW1vcyBsYSBtYXRyaXogZGUgY29uZnVzacOzbiBjb24gbG9zIHJlc3VsdGFkb3MgZGUgbGFzIHByZWRpY2Npb25lcyBwYXJhIHByb2NlZGVyIGEgc3UgYW7DoWxpc2lzIHkgdmFsb3JhY2nDs24uCgpgYGB7cn0KcHJlZF90cmFpbiA8LSBwcmVkaWN0KHJsX2NsYXNzaWZmaWVyLCBuZXdkYXRhID0gdHJhaW5pbmdfc2V0LCB0eXBlID0gInJlc3BvbnNlIikKcHJlZF90cmFpbiA8LSBpZmVsc2UocHJlZF90cmFpbiA+IDAuNSwgInAiLCAiZSIpCnByZWRfdHJhaW4gPC0gZmFjdG9yKHByZWRfdHJhaW4sIGxldmVscyA9IGMoImUiLCAicCIpLCBsYWJlbHMgPSBjKCJlIiwgInAiKSkKCmNvbmZ1c2lvbl9tIDwtIHRhYmxlKHRyYWluaW5nX3NldCRjbGFzcywgcHJlZF90cmFpbikKcHJpbnQoY29uZnVzaW9uX20pCgphY2N1cmFjeSA8LSBzdW0oZGlhZyhjb25mdXNpb25fbSkpIC8gc3VtKGNvbmZ1c2lvbl9tKQpwcmludChhY2N1cmFjeSkKYGBgCgoKVW5hIHZleiBvYnRlbmlkbyBlbCByZXN1bHRhZG8gbW9zdHJhZG8gbWVkaWFudGUgbGEgbWF0cml6IGRlIGNvbmZ1c2nDs24sIHNlIG9ic2VydmEgcXVlIGxhIHByZWRpY2Npw7NuIHBvc2VlIHVuYSBwcmVjaXNpw7NuIGRlbCA3Nyw4JS4KQSBjb250aW51YWNpw7NuLCBzZSBtdWVzdHJhIGVsIHJlc3VsdGFkbyBkZSBhcGxpY2FyIGVsIG1pc21vIHByb2Nlc28gYW50ZXJpb3Igc29icmUgZWwgY29uanVudG8gZGUgcHJ1ZWJhLgoKYGBge3J9CnByZWRfdGVzdCA8LSBwcmVkaWN0KHJsX2NsYXNzaWZmaWVyLCBuZXdkYXRhID0gdGVzdF9zZXQsIHR5cGUgPSAicmVzcG9uc2UiKQpwcmVkX3Rlc3QgPC0gaWZlbHNlKHByZWRfdGVzdCA+IDAuNSwgInAiLCAiZSIpCnByZWRfdGVzdCA8LSBmYWN0b3IocHJlZF90ZXN0LCBsZXZlbHMgPSBjKCJlIiwgInAiKSwgbGFiZWxzID0gYygiZSIsICJwIikpCgpjb25mdXNpb25fbSA8LSB0YWJsZSh0ZXN0X3NldCRjbGFzcywgcHJlZF90ZXN0KQpwcmludChjb25mdXNpb25fbSkKCmFjY3VyYWN5X3JsIDwtIHN1bShkaWFnKGNvbmZ1c2lvbl9tKSkgLyBzdW0oY29uZnVzaW9uX20pCnByaW50KGFjY3VyYWN5X3JsKQpgYGAKClVuYSB2ZXogb2J0ZW5pZG8gZXN0ZSBzZWd1bmRvIHJlc3VsdGFkbywgcG9kZW1vcyBvYnNlcnZhciBxdWUgaGF5IHVuYSBncmFuIHNpbWlsaXR1ZCBlbnRyZSBsb3MgcmVzdWx0YWRvcyBvYnRlbmlkb3MgcGFyYSBhbWJvcyBjb25qdW50b3MgZGUgZGF0b3MsIHJlc3VsdGFuZG8gZXN0YSB2ZXogZW4gdW4gNzcsMyUuCkFudGVzIGRlIGFjYWJhciBkZSBhcGxpY2FyIGVsIGNsYXNpZmljYWRvciBkZSByZWdyZXNpw7NuIGxvZ8Otc3RpY2EsIHZhbW9zIGEgcHJvY2VkZXIgYSBncmFmaWNhciBsYSBjdXJ2YSBST0MsIGxhIGN1YWwgbm9zIGFwb3J0YSBtYXlvciB2aXN1YWxpemFjacOzbiBkZSBsYSByZWxhY2nDs24gZW50cmUgbG9zIGZhbHNvcyB5IHZlcmRhZGVyb3MgcG9zaXRpdm9zLgoKYGBge3J9CmxpYnJhcnkoUk9DUikKcHJlZF9ybF9yb2MgPC0gcHJlZGljdGlvbihhcy5udW1lcmljKHByZWRfdGVzdCksIGFzLm51bWVyaWModGVzdF9zZXQkY2xhc3MpKQpwZXJmX3JsX3JvYyA8LSBwZXJmb3JtYW5jZShwcmVkX3JsX3JvYywgInRwciIsICJmcHIiKQpwZXJmX3JsX2F1YyA8LSBwZXJmb3JtYW5jZShwcmVkX3JsX3JvYywgImF1YyIpCgpwcmludChwZXJmX3JsX2F1Y0B5LnZhbHVlc1tbMV1dKQpwbG90KHBlcmZfcmxfcm9jLCBjb2wgPSAibGlnaHRibHVlIiwgbHdkID0gNSkgIApgYGAKCk9ic2VydmFuZG8gbGEgY3VydmEgUk9DIHJlc3VsdGFudGUgcG9kZW1vcyBjb21lbnRhciBxdWUgc2UgbWFudGllbmUgcG9yIGVuY2ltYSBkZSBsYSBkaWFnb25hbCwgbG8gcXVlIGVzIGJ1ZW5hIHNlw7FhbCwgcGVybyBzZSBhcHJveGltYSBhIGVsbGEsIHB1ZGllbmRvIGhhYmVyIHByb3BvcmNpb25hZG8gcmVzdWx0YWRvcyBtZWpvcmVzIHJlc3VsdGEgc2VyIHVuIG1vZGVsbyBiYXN0YW50ZSBnZW5lcmFsaXphZG8uCgoKIyMjIGstTk48YSBuYW1lPSJrbm4iPjwvYT4KCk1lZGlhbnRlIGVsIGFsZ29yaXRtbyBkZSBjbGFzaWZpY2FjacOzbiBsbGFtYWRvIGstTk4gc2UgYXNpZ25hIHVuYSBudWV2YSBvYnNlcnZhY2nDs24gYSBsYSBjbGFzZSBtw6FzIGNvbcO6biBlbnRyZSBzdXMgImsiIHZlY2lub3MgbcOhcyBjZXJjYW5vcyBlbiBlbCBlc3BhY2lvIGRlIGNhcmFjdGVyw61zdGljYXMuCkFudGVzIGRlIG5hZGEsIHBhcmEgcG9kZXIgYXBsaWNhciBlbCBhbGdvcml0bW8gay1OTiwgZGViZW1vcyB0cmFuc2Zvcm1hciBsYXMgdmFyaWFibGVzIGNhdGVnw7NyaWNhcyBlbiBudW3DqXJpY2FzLgoKYGBge3J9Cm11c2hyb29tX251bSA8LSBkdW1teVZhcnMoIiB+IC4iLCBkYXRhID0gbXVzaHJvb20sIGZ1bGxSYW5rID0gVFJVRSkgJT4lIHByZWRpY3QobXVzaHJvb20pCm11c2hyb29tX251bSA8LSBhcy5kYXRhLmZyYW1lKG11c2hyb29tX251bSkKYGBgCgpBIGNvbnRpbnVhY2nDs24sIHNlIHZpc3VhbGl6YW4gbGFzIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMgeWEgY29kaWZpY2FkYXMgeSBsYXMgZGltZW5zaW9uZXMgZGVsIGRhdGFzZXQuCgpgYGB7cn0KZGltX211c2hyb29tIDwtIGRpbShtdXNocm9vbV9udW0pCnByaW50KGRpbV9tdXNocm9vbSkKc3RyKG11c2hyb29tX251bSkKYGBgCgpEZWJpZG8gYWwgaGVjaG8gZGUgbmVjZXNpdGFyIGxhIHRyYW5zZm9ybWFjacOzbiBkZSBsYXMgdmFyaWFibGVzIGNhdGVnw7NyaWNhcyBlbiBudW3DqXJpY2FzLCBwcm9jZWRlbW9zIGVuIGVzdGUgcGFzbyBhIGRpdmlkaXIgbG9zIGRhdG9zIGVuIGxvcyBjb25qdW50b3MgZGUgZW50cmVuYW1pZW50byB5IGRlIHZhbGlkYWNpw7NuLiBFbiBlc3RlIGNhc28sIGVsIHZhbG9yIDAgY29ycmVzcG9uZGUgY29uIGxvcyBjaGFtcGnDsW9uZXMgY29tZXN0aWJsZXMgeSBlbCB2YWxvciAxIGNvbiBsb3MgY2hhbXBpw7FvbmVzIHZlbmVub3Nvcy4KCmBgYHtyfQpsaWJyYXJ5KGNhVG9vbHMpCnNldC5zZWVkKDE4KQoKc3BsaXQgPC0gc2FtcGxlLnNwbGl0KG11c2hyb29tX251bSRjbGFzcywgU3BsaXRSYXRpbyA9IDAuOCkKdHJhaW5pbmdfc2V0X251bSA8LSBzdWJzZXQobXVzaHJvb21fbnVtLCBzcGxpdCA9PSBUUlVFKQp0ZXN0X3NldF9udW0gPC0gc3Vic2V0KG11c2hyb29tX251bSwgc3BsaXQgPT0gRkFMU0UpCgp0YWJsZSh0cmFpbmluZ19zZXRfbnVtJGNsYXNzKQp0YWJsZSh0ZXN0X3NldF9udW0kY2xhc3MpCmBgYAoKRWwgc2lndWllbnRlIHBhc28gc2Vyw6EgZGV0ZXJtaW5hciBlbCB2YWxvciDDs3B0aW1vIGRlIGsgYW50ZXMgZGUgcHJvY2VkZXIgYSBhcGxpY2FyIGxhIGZ1bmNpw7NuLiBQYXJhIGVsbG8sIHV0aWxpemFyZW1vcyBlbCByZXN1bHRhZG8gcXVlIG5vcyBwcm9wb3JjaW9uYSBlbCBjw6FsY3VsbyBkZSBsYSByYcOteiBjdWFkcmFkYSBkZWwgbsO6bWVybyBkZSBvYnNlcnZhY2lvbmVzIGRlbCBjb25qdW50byBkZSBlbnRyZW5hbWllbnRvLgoKYGBge3J9Cgpucm93c19jbGFzcyA8LSBOUk9XKHRyYWluaW5nX3NldF9udW0pIAprIDwtIHNxcnQobnJvd3NfY2xhc3MpCmsgPC0gcm91bmQoaykKawpgYGAKClVuYSB2ZXogaGVtb3Mgb2J0ZW5pZG8gZWwgdmFsb3IgZGUgaywgcHJvY2VkZW1vcyBhIHJlYWxpemFyIGxhcyBwcmVkaWNjaW9uZXMuIFBhcmEgbGxldmFyIGEgY2FibyBsYSBhcGxpY2FjacOzbiBkZWwgY2xhc2lmaWNhZG9yIGxsYW1hZG8gJ2VsIHZlY2lubyBtw6FzIGNlcmNhbm8nLCBoYXJlbW9zIHVzbyBkZSBsYSBmdW5jacOzbiBrbm4oKSBkZSBsYSBsaWJyZXLDrWEgY2xhc3MuCgpgYGB7cn0KbGlicmFyeShjbGFzcykKc2V0LnNlZWQoMTgpCnByZWRfa25uIDwtIGtubih0cmFpbiA9IHRyYWluaW5nX3NldF9udW1bLCAtMV0sIHRlc3QgPSB0ZXN0X3NldF9udW1bLCAtMV0sIGNsID0gdHJhaW5pbmdfc2V0X251bSRjbGFzcywgayA9IGspCnN1bW1hcnkocHJlZF9rbm4pCmBgYAoKVW5hIHZleiBvYnRlbmlkb3MgbG9zIHJlc3VsdGFkb3MgZGUgbGFzIHByZWRpY2Npb25lcyBlbiBlc3RlIGNhc28sIHBvZGVtb3MgZGUgaWd1YWwgZm9ybWEgY29uc3RydWlyIGxhIG1hdHJpeiBkZSBjb25mdXNpw7NuLgoKYGBge3J9CmNvbmZ1c2lvbl9tIDwtIHRhYmxlKHRlc3Rfc2V0X251bSRjbGFzcywgcHJlZF9rbm4pCmNvbmZ1c2lvbl9tCgphY2N1cmFjeV9rbm4gPC0gc3VtKGRpYWcoY29uZnVzaW9uX20pKSAvIHN1bShjb25mdXNpb25fbSkKYWNjdXJhY3lfa25uCmBgYAoKQ29tbyByZXN1bHRhZG8gZmluYWwgZGUgbGEgbWF0cml6IGRlIGNvbmZ1c2nDs24sIG9idGVuZW1vcyB1bmEgcHJlY2lzacOzbiByZXN1bHRhbnRlIGRlbCA5NyUsIGxhIGN1YWwgbWVqb3JhIHJlc3BlY3RvIGFsIGFsZ29yaXRtbyBkZSBjbGFzaWZpY2FjacOzbiBhbnRlcmlvciwgYXVucXVlIHRlbmRpZW5kbyBhIHBvc2VlciBtYXlvciBzb2JyZWFqdXN0ZS4KRGUgZm9ybWEgbcOhcyB2aXN1YWwsIG9idGVuZW1vcyBhIGNvbnRpbnVhY2nDs24gbGEgZ3LDoWZpY2EgZGUgbGEgY3VydmEgUk9DIHkgZWwgY8OhbGN1bG8gZGVsIMOhcmVhIGJham8gbGEgbWlzbWEuCgpgYGB7cn0KbGlicmFyeShST0NSKQpwcmVkX2tubl9yb2MgPC0gcHJlZGljdGlvbihhcy5udW1lcmljKHByZWRfa25uKSwgYXMubnVtZXJpYyh0ZXN0X3NldF9udW0kY2xhc3MpKQpwZXJmX2tubl9yb2MgPC0gcGVyZm9ybWFuY2UocHJlZF9rbm5fcm9jLCAidHByIiwgImZwciIpCnBlcmZfa25uX2F1YyA8LSBwZXJmb3JtYW5jZShwcmVkX2tubl9yb2MsICJhdWMiKQoKcHJpbnQocGVyZl9rbm5fYXVjQHkudmFsdWVzW1sxXV0pCnBsb3QocGVyZl9rbm5fcm9jLCBjb2wgPSAibGlnaHRibHVlIiwgbHdkID0gNSkgIApgYGAKClVuYSB2ZXogb2J0ZW5lbW9zIGxhIGN1cnZhIFJPQyB5IHN1IMOhcmVhLCBvYnNlcnZhbW9zIHF1ZSBlbCByZXN1bHRhZG8gZXMgbXV5IHBvc2l0aXZvIGVuIGN1YW50byBhIHByZWNpc2nDs24gZGUgbG9zIHJlc3VsdGFkb3MsIHlhIHF1ZSBjb21vIHNlIHB1ZWRlIG9ic2VydmFyIGdyw6FmaWNhbWVudGUgc2UgYWxlamEgZGUgbGEgZGlhZ29uYWwuCgoKIyMjIENsYXNpZmljYWNpw7NuIGNvbiDDgXJib2wgZGUgRGVjaXNpw7NuPGEgbmFtZT0iYXJib2wiPjwvYT4KCkxvcyDDoXJib2xlcyBkZSBkZWNpc2nDs24gc2UgYmFzYW4gZW4gbGEgY29uc3RydWNjacOzbiBkZSByZWdsYXMgbMOzZ2ljYXMgKGRpdmlzaW9uZXMgZGUgbG9zIGRhdG9zIGVudHJlIHJhbmdvcyBvIGNvbmRpY2lvbmVzKSBhIHBhcnRpciBkZSBsb3MgZGF0b3MgZGUgZW50cmFkYS4KUGFyYSB0cmFiYWphciBjb24gZXN0ZSBjbGFzaWZpY2Fkb3IgY29tZW56YW1vcyBhcGxpY2FuZG8gbGEgZnVuY2nDs24gZGVsIMOhcmJvbCBkZSBkZWNpc2nDs24sIHJwYXJ0KCksIHNvYnJlIGVsIGNvbmp1bnRvIGRlIGRhdG9zIGRlIGVudHJlbmFtaWVudG8gY29tbyBzZSBtdWVzdHJhIGEgY29udGludWFjacOzbi4KCmBgYHtyfQpsaWJyYXJ5KHJwYXJ0KQpzZXQuc2VlZCgxOCkKZHRfY2xhc3NpZmZpZXIgPC0gcnBhcnQoY2xhc3MgfiAuLCBkYXRhID0gdHJhaW5pbmdfc2V0KQpgYGAKVW5hIHZleiBvYnRlbmlkbyBlbCByZXN1bHRhZG8sIGxvIGdyYWZpY2FyZW1vcyBwYXJhIGZhY2lsaXRhciBhc8OtIGVsIGFuw6FsaXNpcyBkZWwgcmVzdWx0YWRvLgoKYGBge3J9CmxpYnJhcnkocnBhcnQucGxvdCkKcnBhcnQucGxvdChkdF9jbGFzc2lmZmllcikKYGBgCgpDb25zdHJ1aW1vcyBsYSBtYXRyaXogZGUgY29uZnVzacOzbiBjb24gbG9zIHJlc3VsdGFkb3Mgb2J0ZW5pZG9zIGFudGVyaW9ybWVudGUgcGFyYSBlc3RlIGNhc28uCgpgYGB7cn0KcHJlZF9kdCA8LSBwcmVkaWN0KGR0X2NsYXNzaWZmaWVyLCBuZXdkYXRhID0gdGVzdF9zZXQsIHR5cGUgPSAiY2xhc3MiKQoKY29uZnVzaW9uX20gPC0gdGFibGUodGVzdF9zZXQkY2xhc3MsIHByZWRfZHQpCmNvbmZ1c2lvbl9tCgphY2N1cmFjeV9kdCA8LSBzdW0oZGlhZyhjb25mdXNpb25fbSkpIC8gc3VtKGNvbmZ1c2lvbl9tKQphY2N1cmFjeV9kdApgYGAKVW5hIHZleiB0ZW5lbW9zIGVsIHJlc3VsdGFkbyBwYXJhIGVsIHZhbG9yIGRlIHByZWNpc2nDs24gZW4gbGEgcHJlZGljY2nDs24gYXBsaWNhbmRvIGVsIMOhcmJvbCBkZSBkZWNpc2nDs24sIDgzJSwgcHJvY2VkZW1vcyBhIGNvbnN0cnVpciBsYSBncsOhZmljYSBkZSBsYSBjdXJ2YSBST0MgeSBhIGNhbGN1bGFyIGVsIHZhbG9yIGNvcnJlc3BvbmRpZW50ZSBhbCBBVUMuIEVuIGVzdGUgY2FzbywgY29udGluw7phIHNpZW5kbyBtZWpvciByZXN1bHRhZG8gcXVlIGVsIG9idGVuaWRvIG1lZGlhbnRlIGxhIHJlZ3Jlc2nDs24gbG9nw61zdGljYSwgcHVlc3RvIHF1ZSByZXN1bHRhIGVuIHVuYSBtYXlvciBwcmVjaXNpw7NuLCBwZXJvIGFsIGlndWFsIHF1ZSBlbCBhbGdvcml0bW8gZGUgay1OTiwgdGllbmRlIGEgc2VyIG3DoXMgc29icmVhanVzdGFkby4KCmBgYHtyfQpsaWJyYXJ5KFJPQ1IpCnByZWRfZHRfcm9jIDwtIHByZWRpY3Rpb24oYXMubnVtZXJpYyhwcmVkX2R0KSwgYXMubnVtZXJpYyh0ZXN0X3NldCRjbGFzcykpCnBlcmZfZHRfcm9jIDwtIHBlcmZvcm1hbmNlKHByZWRfZHRfcm9jLCAidHByIiwgImZwciIpCnBlcmZfZHRfYXVjIDwtIHBlcmZvcm1hbmNlKHByZWRfZHRfcm9jLCAiYXVjIikKCnByaW50KHBlcmZfZHRfYXVjQHkudmFsdWVzW1sxXV0pCnBsb3QocGVyZl9kdF9yb2MsIGNvbCA9ICJsaWdodGJsdWUiLCBsd2QgPSA1KSAgCmBgYAoKCiMjIyBDbGFzaWZpY2Fkb3IgUmFuZG9tIEZvcmVzdDxhIG5hbWU9InJhbmRvbSI+PC9hPgoKRWwgYWxnb3JpdG1vIGRlIFJhbmRvbSBGb3Jlc3QgdHJhYmFqYSBtZWRpYW50ZSBsYSBjb21iaW5hY2nDs24gZGUgw6FyYm9sZXMgcHJlZGljdG9yZXMgdGFsIHF1ZSBjYWRhIMOhcmJvbCBkZXBlbmRlIGRlIGxvcyB2YWxvcmVzIGRlIHVuIHZlY3RvciBhbGVhdG9yaW8uCkNvbWVuemFtb3MgYXBsaWNhbmRvIGRpY2hvIGNsYXNpZmljYWRvciBtZWRpYW50ZSBsYSBmdW5jacOzbiBsbGFtYWRhIGRlIGxhIG1pc21hIGZvcm1hOyBlcyBkZWNpciwgcmFuZG9tRm9yZXN0KCkuIEVuIGVzdGEgZnVuY2nDs24sIGVsIHZhbG9yIGNvcnJlc3BvbmRpZW50ZSBhbCBwYXLDoW1ldHJvIGxsYW1hZG8gJ250cmVlJyBpbmRpY2EgbGEgY2FudGlkYWQgZGUgw6FyYm9sZXMgZGUgZGVjaXNpw7NuIHF1ZSBmb3JtYXLDoW4gcGFydGUgZGVsIGNsYXNpZmljYWRvci4KCmBgYHtyfQpsaWJyYXJ5KHJhbmRvbUZvcmVzdCkKc2V0LnNlZWQoMTgpCnJmX2NsYXNzaWZmaWVyIDwtIHJhbmRvbUZvcmVzdChjbGFzcyB+IC4sIGRhdGEgPSB0cmFpbmluZ19zZXQsIG50cmVlID0gMjUwKQpgYGAKCk1lZGlhbnRlIHN1IGdyw6FmaWNhLCB2YW1vcyBhIHByb2NlZGVyIGEgY29tcGFyYXIgbG9zIGVycm9yZXMgZW4gZnVuY2nDs24gZGVsIGF1bWVudG8gZGVsIG7Dum1lcm8gZGUgw6FyYm9sZXM7IGVzIGRlY2lyLCBjdWFudG8gbcOhcyB2YXlhIGF1bWVudGFuZG8gZWwgbsO6bWVybyBkZSDDoXJib2xlcyBoYXN0YSB1biB1bWJyYWwgZGV0ZXJtaW5hZG8sIG1lbm9yIGNhbnRpZGFkIGRlIGVycm9yZXMgcG9zZWVyw6EgbGEgcHJlZGljY2nDs24uCgpgYGB7cn0KcGxvdChyZl9jbGFzc2lmZmllcikKYGBgCgpDYWxjdWxhbW9zIGxhcyBwcmVkaWNjaW9uZXMgc29icmUgZWwgY29uanVudG8gZGUgZGF0b3MgZGUgcHJ1ZWJhIHkgY29uc3RydWltb3MgbGEgbWF0cml6IGRlIGNvbmZ1c2nDs24uCgpgYGB7cn0KcHJlZF9yZiA8LSBwcmVkaWN0KHJmX2NsYXNzaWZmaWVyLCBuZXdkYXRhID0gdGVzdF9zZXQsIHR5cGUgPSAiY2xhc3MiKQoKY29uZnVzaW9uX20gPC0gdGFibGUodGVzdF9zZXQkY2xhc3MsIHByZWRfcmYpCmNvbmZ1c2lvbl9tCgphY2N1cmFjeV9yZiA8LSBzdW0oZGlhZyhjb25mdXNpb25fbSkpIC8gc3VtKGNvbmZ1c2lvbl9tKQphY2N1cmFjeV9yZgpgYGAKClVuYSB2ZXogb2J0ZW5pZG8gZWwgdmFsb3IgZGUgbGEgcHJlY2lzacOzbiBwYXJhIGVzdGUgY2FzbywgZGVmaW5pbW9zIGxhIGN1cnZhIFJPQyB5IHByb2NlZGVtb3MgYSByZWFsaXphciBlbCBjw6FsY3VsbyBkZWwgw6FyZWEgYmFqbyBsYSBjdXJ2YS4gTWVkaWFudGUgZXN0ZSDDumx0aW1vIHBhc28gc2UgcHVlZGUgb2JzZXJ2YXIgc3UgdGFuIGFsdGEgcHJlY2lzacOzbiwgbGEgY3VhbCBpbmRpY2EgZGVtYXNpYWRvIHNvYnJlYWp1c3RlIHNpbiBzZXIgY29udmVuaWVudGUuCgpgYGB7cn0KbGlicmFyeShST0NSKQpwcmVkX3JmX3JvYyA8LSBwcmVkaWN0aW9uKGFzLm51bWVyaWMocHJlZF9yZiksIGFzLm51bWVyaWModGVzdF9zZXQkY2xhc3MpKQpwZXJmX3JmX3JvYyA8LSBwZXJmb3JtYW5jZShwcmVkX3JmX3JvYywgInRwciIsICJmcHIiKQpwZXJmX3JmX2F1YyA8LSBwZXJmb3JtYW5jZShwcmVkX3JmX3JvYywgImF1YyIpCgpwcmludChwZXJmX3JmX2F1Y0B5LnZhbHVlc1tbMV1dKQpwbG90KHBlcmZfcmZfcm9jLCBjb2wgPSAibGlnaHRibHVlIiwgbHdkID0gNSkgIApgYGAKCgojIyMgS2VybmVsIFNWTSBDbGFzc2lmaWVyCgpFbCBjbGFzaWZpY2Fkb3IgZGUgbGEgbcOhcXVpbmEgdmVjdG9yaWFsLCBlbmN1ZW50cmEgbGEgY3VydmEgcXVlIGVzIGNhcGF6IGRlIHNlcGFyYXIgeSBjbGFzaWZpY2FyIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvIGdhcmFudGl6YW5kbyBxdWUgbGEgc2VwYXJhY2nDs24gZW50cmUgw6lzdGEgeSBjaWVydGFzIG9ic2VydmFjaW9uZXMgZGVsIGNvbmp1bnRvIGRlIGVudHJlbmFtaWVudG8gcmVzdWx0ZSBzZXIgbG8gbWF5b3IgcG9zaWJsZS4KUGFyYSBsbGV2YXIgYSBjYWJvIGxhIGFwbGljYWNpw7NuIGRlbCBjbGFzaWZpY2Fkb3IgbGxhbWFkbyAnTcOhcXVpbmEgZGUgU29wb3J0ZSBWZWN0b3JpYWwnIGhhY2Vtb3MgdXNvIGRlIGxhIGZ1bmNpw7NuIHN2bSgpLiBFbiBkaWNoYSBmdW5jacOzbiwgbG9zIHZhbG9yZXMgZGUgbG9zIHBhcsOhbWV0cm9zICd0eXBlJyB5ICdrZXJuZWwnIGhhY2VuIHJlZmVyZW5jaWEgYWwgdGlwbyBkZSBjbGFzaWZpY2Fkb3IgbG8gcXVlIHNpZ25pZmljYSBxdWUgZWwga2VybmVsIHNlcsOhIGRlIHRpcG8gcmFkaWFsIHkgZ2F1c3NpYW5vLiAKCmBgYHtyfQpsaWJyYXJ5KGUxMDcxKQpzZXQuc2VlZCgxOCkKc3ZtX2NsYXNzaWZmaWVyIDwtIHN2bShjbGFzcyB+IC4sCiAgZGF0YSA9IHRyYWluaW5nX3NldCwKICB0eXBlID0gIkMtY2xhc3NpZmljYXRpb24iLCBrZXJuZWwgPSAicmFkaWFsIgopCmBgYApBIGNvbnRpbnVhY2nDs24sIHNlIGNhbGN1bGEgbGEgcHJlZGljY2nDs24geSBzZSBjb25zdHJ1eWUgbGEgbWF0csOteiBkZSBjb25mdXNpw7NuLCBsYXMgY3VhbGVzIHJlc3VsdGFuIHNlciBsYXMgc2lndWllbnRlcy4KCmBgYHtyfQpwcmVkX3N2bSA8LSBwcmVkaWN0KHN2bV9jbGFzc2lmZmllciwgbmV3ZGF0YSA9IHRlc3Rfc2V0LCB0eXBlID0gImNsYXNzIikKCmNvbmZ1c2lvbl9tIDwtIHRhYmxlKHRlc3Rfc2V0JGNsYXNzLCBwcmVkX3N2bSkKY29uZnVzaW9uX20KCmFjY3VyYWN5X3N2bSA8LSBzdW0oZGlhZyhjb25mdXNpb25fbSkpIC8gc3VtKGNvbmZ1c2lvbl9tKQphY2N1cmFjeV9zdm0KYGBgCgpDb21vIHNlIHB1ZWRlIG9ic2VydmFyLCBlbCB2YWxvciBkZSBsYSBwcmVjaXNpw7NuIGVuIGxhIHByZWRpY2Npw7NuIGVuIGVzdGUgY2FzbyByZXN1bHRhIHNlciBkZWwgOTUlLCB1biByZXN1bHRhZG8gYnVlbm8gcXVlIG5vIG11ZXN0cmEgc2XDsWFsZXMgZGUgc29icmVhanVzdGUuClBhcmEgZmluYWxpemFyLCBjb25zdHJ1aW1vcyBsYSBjdXJ2YSBST0MgY29ycmVzcG9uZGllbnRlIGVuIGVzdGUgY2FzbyB5IGNhbGN1bGFtb3Mgc3Ugw6FyZWEuCgpgYGB7cn0KbGlicmFyeShST0NSKQpwcmVkX3N2bV9yb2MgPC0gcHJlZGljdGlvbihhcy5udW1lcmljKHByZWRfc3ZtKSwgYXMubnVtZXJpYyh0ZXN0X3NldCRjbGFzcykpCnBlcmZfc3ZtX3JvYyA8LSBwZXJmb3JtYW5jZShwcmVkX3N2bV9yb2MsICJ0cHIiLCAiZnByIikKcGVyZl9zdm1fYXVjIDwtIHBlcmZvcm1hbmNlKHByZWRfc3ZtX3JvYywgImF1YyIpCgpwcmludChwZXJmX3N2bV9hdWNAeS52YWx1ZXNbWzFdXSkKcGxvdChwZXJmX3N2bV9yb2MsIGNvbCA9ICJsaWdodGJsdWUiLCBsd2QgPSA1KSAgCmBgYAoKCiMjIyBDb25jbHVzaW9uZXMKClVuYSB2ZXogYXBsaWNhZG9zIGxvcyBjaW5jbyBkaXN0aW50b3MgbcOpdG9kb3MgZGUgY2xhc2lmaWNhY2nDs24gc29icmUgbnVlc3RybyBkYXRhc2V0IGxsYW1hZG8gJ211c2hyb29tJyB0cmFzIGhhYmVyIHJlYWxpemFkbyBhbnRlcyBzdSBwcmVwcm9jZXNhbWllbnRvLCBwb2RlbW9zIGNvbmNsdWlyIGRpY2llbmRvIHF1ZSBlbCBjbGFzaWZpY2Fkb3IgZGUgUmFuZG9tIEZvcmVzdCBlcyBlbCBxdWUgaGEgcmVzdWx0YWRvIHBvc2VlciB1biBtYXlvciB2YWxvciBlbiBsYSBwcmVjaXNpw7NuIGRlIGxhIHByZWRpY2Npw7NuIGVuIGxhIGNsYXNpZmljYWNpw7NuIHksIHBvciBsbyB0YW50bywgdW4gbWVub3IgdmFsb3IgcGFyYSBlbCBlcnJvciBkZSBwcmVkaWNjacOzbi4gU2luIGVtYmFyZ28sIGFsIGFwbGljYXIgZXN0ZSBhbGdvcml0bW8gaGVtb3Mgb2J0ZW5pZG8gdW4gbWF5b3Igc29icmVhanVzdGUsIGVsIGN1YWwgbm8gYmVuZWZpY2lhIGFsIG1vZGVsbyB5YSBxdWUgc2UgYnVzY2EgcXVlIGxvcyByZXN1bHRhZG9zIG9idGVuaWRvcyBzZWFuIHByZWNpc29zLCBwZXJvIHRhbWJpw6luIGdlbmVyYWxpemFkb3MgcGFyYSBsb3MgZGF0b3MuIFBvciBlc3RhIHJhesOzbiwgY29uc2lkZXJhbW9zIHF1ZSByZXN1bHRhIG3DoXMgYmVuZWZpY2lvc28gc2FjcmlmaWNhciBwYXJ0ZSBkZWwgdmFsb3IgZGUgcHJlY2lzacOzbiwgdGVuaWVuZG8gZW4gY3VlbnRhIGFsZ3Vub3MgdmFsb3JlcyBkZSBmYWxzb3MgcG9zaXRpdm9zIHkgZmFsc29zIG5lZ2F0aXZvcywgY29tbyBvY3VycmUgZW4gZWwgY2FzbyBkZSBsb3MgY2xhc2lmaWNhZG9yZXMgZGUgbGEgTcOhcXVpbmEgZGUgU29wb3J0ZSBWZWN0b3JpYWwgbyBrLU5OLCBhcHJvdmVjaGFuZG8gYXPDrSBzdSBjYXBhY2lkYWQgZGUgbWF5b3IgZ2VuZXJhbGl6YWNpw7NuLgpTb2JyZSBsb3MgZ3LDoWZpY29zIHF1ZSBzZSBtdWVzdHJhbiBhIGNvbnRpbnVhY2nDs24gc2UgcHVlZGVuIGNvbXBhcmFyIGxvcyBkaXN0aW50b3Mgbml2ZWxlcyBkZSBwcmVjaXNpw7NuIHkgQVVDIHBhcmEgY2FkYSB1bm8gZGUgbG9zIGRpZmVyZW50ZXMgY2xhc2lmaWNhZG9yZXMgcXVlIGhhbiBzaWRvIGFwbGljYWRvcy4KCmBgYHtyfQphY2N1cmFjeV9jb21wIDwtIG1hdHJpeChjKGFjY3VyYWN5X3JsLCBhY2N1cmFjeV9rbm4sIGFjY3VyYWN5X2R0LCBhY2N1cmFjeV9yZiwgYWNjdXJhY3lfc3ZtKSwgbmNvbCA9IDUpCgpiYXJwbG90KGFjY3VyYWN5X2NvbXAsCiAgbWFpbiA9ICJBY2N1cmFjeSBDb21wYXJpc29uIiwKICB4bGFiID0gIkFjY3VyYWN5ICglKSIsCiAgeWxhYiA9ICJNZXRob2QiLAogIG5hbWVzLmFyZyA9IGMoIlJMIiwgIkstTk4iLCAiRFQiLCAiUkYiLCAiU1ZNIiksCiAgY29sID0gIiM3ZmQ2ZDkiCikKYGBgCgpgYGB7cn0KcGVyZl9hdWMgPC0gbWF0cml4KGMocGVyZl9ybF9hdWNAeS52YWx1ZXNbWzFdXSwgcGVyZl9rbm5fYXVjQHkudmFsdWVzW1sxXV0sIHBlcmZfZHRfYXVjQHkudmFsdWVzW1sxXV0sIHBlcmZfcmZfYXVjQHkudmFsdWVzW1sxXV0sIHBlcmZfc3ZtX2F1Y0B5LnZhbHVlc1tbMV1dKSwgbmNvbCA9IDUpCgpiYXJwbG90KHBlcmZfYXVjLAogIG1haW4gPSAiQVVDIENvbXBhcmlzb24iLAogIHhsYWIgPSAiQVVDICglKSIsCiAgeWxhYiA9ICJNZXRob2QiLAogIG5hbWVzLmFyZyA9IGMoIlJMIiwgIkstTk4iLCAiRFQiLCAiUkYiLCAiU1ZNIiksCiAgY29sID0gIiM3ZmQ2ZDkiCikKYGBgCgoKIyMgQW7DoWxpc2lzIGFwcmVuZGl6YWplIG5vIHN1cGVydmlzYWRvPGEgbmFtZT0idW5zdXBlcnZpc2VkIj48L2E+CgpFbiBlc3RlIGFwYXJ0YWRvIHNlIGFuYWxpemFyw6EgZWwgZGF0YXNldCBhIHRyYXbDqXMgZGUgYWxnb3JpdG1vcyBkZSBhcHJlbmRpemFqZSBubyBzdXBlcnZpc2Fkby4gRW4gY29uY3JldG8sIHNlIHByb2JhcsOhbiBsb3MgYWxnb3JpdG1vcyBrLW1lYW5zIHkgY2x1c3RlcmluZyBqZXLDoXJxdWljby4KUGFyYSBhbWJvcyBhbGdvcml0bW9zLCBzZSBzZWd1aXLDoSBlbCBzaWd1aWVudGUgZXNxdWVtYToKClBhcmEgYW1ib3MgYWxnb3JpdG1vcyBzZSBsbGV2YXLDoSBhIGNhYm8gZWwgc2lndWllbnRlIHByb2Nlc286CgoxLiBTZSByZXByZXNlbnRhcsOhIGdyw6FmaWNhbWVudGUgbGEgZGlzdHJpYnVjacOzbiBpbmljaWFsIGRlIGxvcyBkYXRvcyBlbiBlbCBkYXRhc2V0LgoyLiBTZSBkZXRlcm1pbmFyw6EgZWwgbsO6bWVybyDDs3B0aW1vIGRlIGNsw7pzdGVyZXMgYSB1dGlsaXphciBwYXJhIGRpdmlkaXIgbG9zIGRhdG9zIGRlIGZvcm1hIGFkZWN1YWRhLgozLiBTZSByZXByZXNlbnRhcsOhIGdyw6FmaWNhbWVudGUgbGEgZGlzdHJpYnVjacOzbiBkZSBsb3MgZGF0b3MgZW4gZnVuY2nDs24gZGVsIG7Dum1lcm8gZGUgY2zDunN0ZXJlcyBlbGVnaWRvLgo0LiBTZSBjYWxjdWxhcsOhIGVsIHByb21lZGlvIGRlIGNhZGEgdW5hIGRlIGxhcyB2YXJpYWJsZXMgZW4gZWwgZGF0YXNldCBwYXJhIGNhZGEgdW5vIGRlIGxvcyBjbMO6c3RlcmVzIHJlc3VsdGFudGVzLgo1LiBGaW5hbG1lbnRlLCB5IGdyYWNpYXMgYSB0ZW5lciBpbmZvcm1hY2nDs24gYSBwcmlvcmkgZGUgbGEgY2xhc2UgZGUgY2FkYSBjaGFtcGnDscOzbiwgc2UgY2FsY3VsYXLDoSBlbCAiYWNjdXJhY3kiIGRlbCBhbGdvcml0bW8sIG1pZGllbmRvIGPDs21vIGRlIGJpZW4gc2UgZXN0w6EgcmVhbGl6YW5kbyBsYSB0YXJlYSBkZSBkaXZpZGlyIGxvcyBkYXRvcyBlbiBjbMO6c3RlcmVzIGRlIGZvcm1hIGFkZWN1YWRhLgoKUGFyYSBwb2RlciB0cmFiYWphciBjb24gYWxnb3JpdG1vcyBubyBzdXBlcnZpc2Fkb3Mgc2Vyw6EgbmVjZXNhcmlvIHF1ZSBsYXMgdmFyaWFibGVzIHNlYW4gbnVtw6lyaWNhcy4gUGFyYSBlbGxvLCBzZSBlbGltaW5hcsOhbiBsYXMgdmFyaWFibGVzIGNhdGVnw7NyaWNhcyBkZWwgZGF0YXNldC4KYGBge3J9Cm51bWVyaWNhbF9jb2x1bW5zIDwtIG11c2hyb29tWywgbnVtZXJpY2FsX2ZlYXR1cmVzXQpgYGAKCkFudGVzIGRlIGNvbWVuemFyIGNvbiBsb3MgYWxnb3JpdG1vcyBubyBzdXBlcnZpc2Fkb3MsIHJlcHJlc2VudGFyZW1vcyBkZSBmb3JtYSBncsOhZmljYSBsYSBkaXN0cmlidWNpw7NuIGluaWNpYWwgZGUgbG9zIGRhdG9zIGEgdHJhdsOpcyBkZSB1biBkaWFncmFtYSBkZSBkaXNwZXJzacOzbiAzRCwgZMOzbmRlIGNhZGEgcHVudG8gcmVwcmVzZW50YSB1biBjaGFtcGnDscOzbiB5IGxhcyB2YXJpYWJsZXMgcXVlIHNlIHJlcHJlc2VudGFuIHNvbiBlbCBkacOhbWV0cm8gZGVsIHNvbWJyZXJvLCBsYSBhbHR1cmEgZGVsIHRhbGxvIHkgZWwgYW5jaG8gZGVsIHRhbGxvLiBDYWRhIHZhcmlhYmxlIGVzdMOhIG5vcm1hbGl6YWRhIGVudHJlIDAgeSAxLgpgYGB7cn0KZGYgPC0gYXMuZGF0YS5mcmFtZShudW1lcmljYWxfY29sdW1ucykKCnBsb3RfbHkoZGYsCiAgeCA9IH5jYXAuZGlhbWV0ZXIsIHkgPSB+c3RlbS5oZWlnaHQsCiAgeiA9IH5zdGVtLndpZHRoCikgJT4lCiAgYWRkX21hcmtlcnMoc2l6ZSA9IDEuNSkKYGBgCgojIyMgSy1tZWFuczxhIG5hbWU9ImttZWFucyI+PC9hPgoKRWwgYWxnb3JpdG1vIGstbWVhbnMgZXMgdW4gbcOpdG9kbyBkZSBjbHVzdGVyaW5nIHF1ZSBwZXJtaXRlIGRpdmlkaXIgdW4gY29uanVudG8gZGUgZGF0b3MgZW4gayBncnVwb3MgbyBjbMO6c3RlcmVzIGRlIG1hbmVyYSBxdWUgbG9zIHB1bnRvcyBkZW50cm8gZGUgdW4gbWlzbW8gY2zDunN0ZXJlcyBzZWFuIHNpbWlsYXJlcyBlbnRyZSBzw60geSBkaWZlcmVudGVzIGEgbG9zIHB1bnRvcyBkZSBsb3MgZGVtw6FzIGNsw7pzdGVyZXMuIExhIGZ1bmNpw7NuIGttZWFucygpIGRlIGxhIGxpYnJlcsOtYSBjbMO6c3RlcmVzIHVuYSBpbXBsZW1lbnRhY2nDs24gZGVsIGFsZ29yaXRtbyBrLW1lYW5zIGVuIFIuCgpQYXJhIHV0aWxpemFyIGxhIGZ1bmNpw7NuIGttZWFucygpLCBlcyBuZWNlc2FyaW8gZXNwZWNpZmljYXIgZWwgbsO6bWVybyBkZSBjbMO6c3RlcmVzIHF1ZSBzZSBkZXNlYW4gb2J0ZW5lciwgcXVlIHNlIGluZGljYSBhIHRyYXbDqXMgZGVsIHBhcsOhbWV0cm8gImNlbnRlcnMiLiAKClBvciBvdHJvIGxhZG8sIGVsIHBhcsOhbWV0cm8gIm5zdGFyIiBpbmRpY2EgZWwgbsO6bWVybyBkZSB2ZWNlcyBxdWUgc2UgZGVzZWEgcmVhbGl6YXIgZWwgcHJvY2VzbyBkZSBjbHVzdGVyaW5nLiBDYWRhIHZleiBxdWUgc2UgZWplY3V0YSBlbCBwcm9jZXNvLCBzZSB1dGlsaXphIHVuIGNvbmp1bnRvIGRpZmVyZW50ZSBkZSBzZW1pbGxhcyBpbmljaWFsZXMgcGFyYSBsb3MgY2VudHJvaWRlcyBkZSBsb3MgY2zDunN0ZXJlcyB5IHNlIG9idGllbmUgdW4gcmVzdWx0YWRvIGRpZmVyZW50ZS4gQWwgZXNwZWNpZmljYXIgdW4gdmFsb3IgcGFyYSAibnN0YXJ0IiBtYXlvciBxdWUgMSwgc2Ugb2J0aWVuZW4gdmFyaW9zIHJlc3VsdGFkb3MgZGlmZXJlbnRlcyB5IHNlIHNlbGVjY2lvbmEgZWwgcXVlIG1pbmltaXphIGxhIHN1bWEgZGUgY3VhZHJhZG9zIHRvdGFsLiBQb3IgdGFudG8sIGVzdGFibGVjZW1vcyAibnN0YXJ0IiBhIDIwIHBhcmEgcXVlIHNlIHJlYWxpY2UgZWwgcHJvY2VzbyAyMCB2ZWNlcyB5IHNlIG9idGVuZ2EgdW4gcmVzdWx0YWRvIG3DoXMgcm9idXN0by4KClVuYSB2ZXogcXVlIHNlIGhhIGVqZWN1dGFkbyBsYSBmdW5jacOzbiBjb24gdW4gZGV0ZXJtaW5hZG8gdmFsb3IgZGUgImNlbnRlcnMiLCBzZSBwdWVkZSBjYWxjdWxhciBsYSBzdW1hIGRlIGN1YWRyYWRvcyBpbnRlcm5vcyAod2l0aGluIGdyb3VwcyBzdW0gb2Ygc3F1YXJlcykgcGFyYSBlc2UgdmFsb3IgZGUgImNlbnRlcnMiLiBMYSBzdW1hIGRlIGN1YWRyYWRvcyBpbnRlcm5vcyBlcyB1bmEgbWVkaWRhIGRlIGxhIHZhcmlhYmlsaWRhZCBkZSBsb3MgZGF0b3MgZGVudHJvIGRlIGNhZGEgY2x1c3Rlci4gQ3VhbnRvIG1heW9yIHNlYSBsYSBzdW1hIGRlIGN1YWRyYWRvcyBpbnRlcm5vcywgbcOhcyBkaXNwZXJzb3MgZXN0YXLDoW4gbG9zIGRhdG9zIGRlbnRybyBkZWwgY2zDunN0ZXJ5LCBwb3IgdGFudG8sIG1lbm9zIGhvbW9nw6luZW8gc2Vyw6EgZWwgY2x1c3Rlci4KClBhcmEgZGV0ZXJtaW5hciBlbCBuw7ptZXJvIMOzcHRpbW8gZGUgY2zDunN0ZXJlcywgc2UgcHVlZGUgdXRpbGl6YXIgZWwgbcOpdG9kbyBkZWwgY29kbywgcXVlIGNvbnNpc3RlIGVuIHJlcHJlc2VudGFyIGxhIHN1bWEgZGUgY3VhZHJhZG9zIGludGVybm9zIGVuIGZ1bmNpw7NuIGRlbCBuw7ptZXJvIGRlIGNsw7pzdGVyZXMgeSBzZWxlY2Npb25hciBlbCBuw7ptZXJvIGRlIGNsw7pzdGVyZXMgZW4gZWwgcXVlIHNlIHByb2R1Y2UgdW4gImNvZG8iIGVuIGxhIGdyw6FmaWNhLiBFc3RlICJjb2RvIiBzdWVsZSBjb3JyZXNwb25kZXIgYWwgcHVudG8gZW4gZWwgcXVlIGxhIGRpc21pbnVjacOzbiBkZSBsYSBzdW1hIGRlIGN1YWRyYWRvcyBpbnRlcm5vcyBzZSB2dWVsdmUgbcOhcyBsZW50YSB5LCBwb3IgdGFudG8sIGEgcGFydGlyIGRlbCBjdWFsIG5vIHNlIG9idGllbmVuIG1lam9yYXMgc2lnbmlmaWNhdGl2YXMgZW4gbGEgY2FsaWRhZCBkZWwgY2x1c3RlcmluZy4KCmBgYHtyfQp3c3NfcGVyX2sgPC0gMApmb3IgKGkgaW4gMToxMCkgewogIGttZWFuc19hdXggPC0ga21lYW5zKG51bWVyaWNhbF9jb2x1bW5zLCBjZW50ZXIgPSBpLCBuc3RhciA9IDIwKQogIHdzc19wZXJfa1tpXSA8LSBrbWVhbnNfYXV4JHRvdC53aXRoaW5zcwp9CnBhcihtZnJvdyA9IGMoMSwgMSkpCnBsb3QoMToxMCwgd3NzX3Blcl9rLAogIHR5cGUgPSAiYiIsCiAgeGxhYiA9ICJOdW1iZXIgb2YgY2x1c3RlcnMiLAogIHlsYWIgPSAiV1NTIiwKKQpgYGAKCkNvbW8gc2UgcHVlZGUgb2JzZXJ2YXIgZW4gbGEgZ3LDoWZpY2EgYW50ZXJpb3IsIGxhIHN1bWEgZGUgY3VhZHJhZG9zIGludGVybm9zIGRpc21pbnV5ZSBhIG1lZGlkYSBxdWUgYXVtZW50YSBlbCBuw7ptZXJvIGRlIGNsw7pzdGVyZXMuIFNpbiBlbWJhcmdvLCBhIHBhcnRpciBkZSAyIGNsw7pzdGVyZXMsIGxhIGRpc21pbnVjacOzbiBkZSBsYSBzdW1hIGRlIGN1YWRyYWRvcyBpbnRlcm5vcyBlcyBtw6FzIHBlcXVlw7FhLiBQb3IgbG8gdGFudG8sIHNlIGRlY2lkZSBoYWNlciB1c28gMiBjbMO6c3RlcmVzLiBFbiBlc3RlIGNhc28gZXNwZWPDrWZpY28sIHRpZW5lIHNlbnRpZG8gdXRpbGl6YXIgMiBjbMO6c3RlcmVzLCB5YSBxdWUgY29ub2NlbW9zIHF1ZSBlbCBkYXRhc2V0IGVzIGJpbmFyaW8uCgpVbmEgdmV6IGRldGVybWluYWRvIGVsIG7Dum1lcm8gZGUgY2zDunN0ZXJlcywgZ2VuZXJhbW9zIGVsIG1vZGVsbyBkZSBrLW1lYW5zLgoKYGBge3J9CmttX21vZGVsIDwtIGttZWFucyhkZiwgY2VudGVyID0gMiwgbnN0YXIgPSAyMCkKYGBgCgpQYXJhIHBvZGVyIHZpc3VhbGl6YXIgbG9zIHJlc3VsdGFkb3MsIHNlIGHDsWFkZSB1bmEgbnVldmEgY29sdW1uYSBhbCBkYXRhc2V0IGNvbiBlbCBuw7ptZXJvIGRlIGNsw7pzdGVyIGFsIHF1ZSBwZXJ0ZW5lY2UgY2FkYSBvYnNlcnZhY2nDs24uIFVuYSB2ZXogYcOxYWRpZGEsIHNlIHB1ZWRlIHJlcHJlc2VudGFyIGxhIGRpc3RyaWJ1Y2nDs24gZGUgbG9zIGRhdG9zIGVuIGZ1bmNpw7NuIGRlIGxvcyBjbMO6c3RlcmVzIG9idGVuaWRvcy4KYGBge3J9CmRmJGNsdXN0ZXI8LSBmYWN0b3Ioa21fbW9kZWwkY2x1c3RlcikKCnBsb3RfbHkoZGYsCiAgeCA9IH5jYXAuZGlhbWV0ZXIsIHkgPSB+c3RlbS5oZWlnaHQsCiAgeiA9IH5zdGVtLndpZHRoLCBjb2xvciA9IH5jbHVzdGVyCikgJT4lCiAgYWRkX21hcmtlcnMoc2l6ZSA9IDEuNSkKYGBgCgpTZSBwdWVkZSBvYnNlcnZhciBxdWUgbG9zIGNoYW1wacOxb25lcyBkZSBtZW5vciB0YW1hw7FvIChlbiBkacOhbWV0cm8sIGFsdHVyYSB5IGFuY2h1cmEpIHBlcnRlbmVjZW4gYWwgY2zDunN0ZXIgMiB5IGxvcyBkZSBtYXlvciB0YW1hw7FvIHBlcnRlbmVjZW4gYWwgY2zDunN0ZXIgMS4KCkEgY29udGludWFjacOzbiwgY2FsY3VsYXJlbW9zIGVsIHZhbG9yIHByb21lZGlvIGRlIGxhcyB2YXJpYWJsZXMgcGFyYSBjYWRhIGNsw7pzdGVyIGdlbmVyYWRvIGNvbiBlbCBtb2RlbG8gZGUgay1tZWFucy4gUGFyYSBlbGxvLCB1dGlsaXphcmVtb3MgbGEgZnVuY2nDs24gZ3JvdXBfYnkoKSBkZSBsYSBsaWJyZXLDrWEgZHBseXIgcGFyYSBhZ3J1cGFyIGxvcyBkYXRvcyBwb3IgY2zDunN0ZXIgeSBsYSBmdW5jacOzbiBzdW1tYXJpc2UoKSBwYXJhIGNhbGN1bGFyIGVsIHZhbG9yIHByb21lZGlvIGRlIGNhZGEgdmFyaWFibGUuIEVzIGltcG9ydGFudGUgZGVzdGFjYXIgcXVlLCBlbiBlc3RlIGNhc28sIGxvcyBkYXRvcyBlc3TDoW4gbm9ybWFsaXphZG9zLCBwb3IgbG8gcXVlIGVsIHZhbG9yIHByb21lZGlvIGRlIGNhZGEgdmFyaWFibGUgbm8gdGllbmUgdW4gc2lnbmlmaWNhZG8gcmVhbC4gU2luIGVtYmFyZ28sIG5vcyBwZXJtaXRlIGNvbXBhcmFyIGxvcyB2YWxvcmVzIGRlIGNhZGEgdmFyaWFibGUgcGFyYSBjYWRhIGNsdXN0ZXIuCmBgYHtyfQpncm91cGVkX211c2hyb29tIDwtIGRmICU+JQogIGdyb3VwX2J5KGNsdXN0ZXIpICU+JQogIHN1bW1hcmlzZSgKICAgIG1lYW5fY2FwX2RpYW1ldGVyID0gbWVhbihjYXAuZGlhbWV0ZXIpLAogICAgbWVhbl9zdGVtX2hlaWdodCA9IG1lYW4oc3RlbS5oZWlnaHQpLAogICAgbWVhbl9zdGVtX3dpZHRoID0gbWVhbihzdGVtLndpZHRoKQogICkKCmdyb3VwZWRfbXVzaHJvb20KYGBgCgpPYnNlcnZhbW9zIHF1ZSBsb3MgdmFsb3JlcyBkZWwgY2zDunN0ZXIgMSBzb24gbWF5b3JlcyBxdWUgbG9zIGRlbCBjbMO6c3RlciAyLCBsbyBxdWUgaW5kaWNhIHF1ZSBsb3MgY2hhbXBpw7FvbmVzIGRlbCBjbMO6c3RlciAxIHNvbiBkZSBtYXlvciB0YW1hw7FvIHF1ZSBsb3MgZGVsIGNsw7pzdGVyIDIuIAoKQSBwYXJ0aXIgZGUgZXN0ZSBtb21lbnRvLCBoZW1vcyBkZWNpZGlkbyBtb2RpZmljYXIgZWwgZGF0YXNldCBhY3R1YWwgZGViaWRvIGEgcXVlIHBhcmEgYXBsaWNhciB0w6ljbmljYXMgY29tbyAic2lsaG91ZXR0ZSIgbyAiZGVuZHJvZ3JhbSIgKHBhcmEgZWwgY2FzbyBkZSBjbHVzdGVyaW5nIGplcsOhcnF1aWNvKSBlcyBuZWNlc2FyaW8gcXVlIGVsIGRhdGFzZXQgc2VhIGRlIG1lbm9yIHRhbWHDsW8uCkxhIGZ1bmNpw7NuIGNyZWF0ZURhdGFQYXJ0aXRpb24oKSBkZSBsYSBsaWJyZXLDrWEgY2FyZXQgZW4gUiBwZXJtaXRlIGRpdmlkaXIgdW4gY29uanVudG8gZGUgZGF0b3MgZW4gZG9zIGdydXBvcywgdW5vIGRlIGVudHJlbmFtaWVudG8geSBvdHJvIGRlIHZhbGlkYWNpw7NuLCBkZSBtYW5lcmEgZXN0cmF0aWZpY2FkYSwgZXMgZGVjaXIsIG1hbnRlbmllbmRvIGxhIHByb3BvcmNpw7NuIGRlIGVsZW1lbnRvcyBkZSBjYWRhIGNsYXNlIGVuIGFtYm9zIGdydXBvcy4gRW4gZXN0ZSBjYXNvLCBzZSBlc3TDoSBlc3BlY2lmaWNhbmRvIHF1ZSBzZSBkZXNlYSBtYW50ZW5lciDDum5pY2FtZW50ZSBlbCAxJSBkZSBsb3MgZGF0b3MgaW5pY2lhbGVzIHBhcmEgZWwgYW7DoWxpc2lzLCBsbyBxdWUgaW1wbGljYSBxdWUgc2UgZXN0w6EgdXRpbGl6YW5kbyBjcmVhdGVEYXRhUGFydGl0aW9uKCkgcGFyYSByZWR1Y2lyIGVsIHRhbWHDsW8gZGVsIGNvbmp1bnRvIGRlIGRhdG9zIGVuIGx1Z2FyIGRlIHBhcmEgZGl2aWRpcmxvIGVuIGRvcyBncnVwb3MuIEFsIHV0aWxpemFyIGNyZWF0ZURhdGFQYXJ0aXRpb24oKSBkZSBlc3RhIG1hbmVyYSwgc2UgbWFudGllbmUgbGEgcHJvcG9yY2nDs24gZGUgZWxlbWVudG9zIGRlIGNhZGEgY2xhc2UgZW4gZWwgY29uanVudG8gZGUgZGF0b3MgcmVkdWNpZG8uCgpBbnRlcyBkZSByZWR1Y2lyIGVsIGRhdGFzZXQsIGVzIG5lY2VzYXJpbyBjb252ZXJ0aXIgbGFzIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMgZW4gbnVtw6lyaWNhcyBhIHRyYXbDqXMgZGUgdmFyaWFibGVzIGR1bW15LiBQYXJhIGVsbG8sIHV0aWxpemFtb3MgbGEgZnVuY2nDs24gZHVtbXlWYXJzKCkgZGUgbGEgbGlicmVyw61hIGNhcmV0IHBhcmEgY3JlYXIgdW4gb2JqZXRvIGRlIHRpcG8gZHVtbXlWYXJzIHkgbGEgZnVuY2nDs24gcHJlZGljdCgpIHBhcmEgY3JlYXIgdW4gbnVldm8gZGF0YXNldCBjb24gbGFzIHZhcmlhYmxlcyBkdW1teS4KCmBgYHtyfQptdXNocm9vbSA8LSBkdW1teVZhcnMoIiB+IC4iLCBkYXRhID0gbXVzaHJvb20sIGZ1bGxSYW5rID0gVFJVRSkgJT4lIHByZWRpY3QobXVzaHJvb20pCm11c2hyb29tIDwtIGFzLmRhdGEuZnJhbWUobXVzaHJvb20pCgpzZXQuc2VlZCg0MikKc3BsaXQgPC0gY3JlYXRlRGF0YVBhcnRpdGlvbihtdXNocm9vbSRjbGFzcywgcCA9IDAuMDEpCnNtYWxsZXJfZGYgPC0gbXVzaHJvb21bc3BsaXQkUmVzYW1wbGUxLCBdCmBgYAoKQ29tcHJvYmFtb3MgcXVlIGxhIHByb3BvcmNpw7NuIGRlIGRhdG9zIGRlIGNhZGEgY2xhc2Ugc2UgbWFudGllbmUgYWwgaGFjZXIgbGEgcGFydGljacOzbi4KYGBge3J9CmluaXRpYWxfY2xhc3NfcHJvcCA8LSB0YWJsZShtdXNocm9vbSRjbGFzcykgLyBucm93KG11c2hyb29tKQpzbWFsbGVyX2NsYXNzX3Byb3AgPC0gdGFibGUoc21hbGxlcl9kZiRjbGFzcykgLyBucm93KHNtYWxsZXJfZGYpCgpwcmludChpbml0aWFsX2NsYXNzX3Byb3ApCnByaW50KHNtYWxsZXJfY2xhc3NfcHJvcCkKYGBgCgpNb3N0cmFtb3MgZGUgZm9ybWEgZ3LDoWZpY2EgbGFzIG51ZXZhcyBwcm9wb3JjaW9uZXMgZGUgbGEgdmFyaWFibGUgZGVwZW5kaWVudGUgImNsYXNzIiBlbiBlbCBkYXRhc2V0IHJlZHVjaWRvLgpgYGB7cn0KcHJpbnQoZ2dwbG90KHNtYWxsZXJfZGYsIGFlc19zdHJpbmcoeCA9IHNtYWxsZXJfZGYkY2xhc3MpKSArCiAgZ2VvbV9iYXIoZmlsbCA9ICIjN2ZkNmQ5IikgKwogIGdlb21fdGV4dChzdGF0ID0gImNvdW50IiwgYWVzKGxhYmVsID0gc2NhbGVzOjpwZXJjZW50KC4uY291bnQuLiAvIG5yb3coc21hbGxlcl9kZikpLCB2anVzdCA9IC0wLjI1KSkgKwogIGxhYnMoeCA9IGksIHkgPSAiUGVyY2VudGFnZSIpICsKICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDkwLCBoanVzdCA9IDEpKSkKYGBgCgoKVW5hIHZleiByZWR1Y2lkbyBlbCBkYXRhc2V0LCB2YW1vcyBhIGVsaW1pbmFyIGxhcyB2YXJpYWJsZXMgY2F0ZWfDs3JpY2FzIHBhcmEgcG9kZXIgYXBsaWNhciB0w6ljbmljYXMgZGUgY2x1c3RlcmluZy4KYGBge3J9CnNtYWxsZXJfZGYgPC0gc21hbGxlcl9kZlssIG51bWVyaWNhbF9mZWF0dXJlc10KZGltKHNtYWxsZXJfZGYpCmBgYAoKVHJhcyBlamVjdXRhciBsYSBjZWxkYSBhbnRlcmlvciBzZSBwdWVkZSBvYnNlcnZhciBxdWUgZWwgZGF0YXNldCBoYSBwYXNhZG8gYSB0ZW5lciA2MTEgb2JzZXJ2YWNpb25lcyB5IDMgdmFyaWFibGVzLgoKQ29tbyBub3MgZW5jb250cmFtb3MgYW50ZSB1biBkYXRhc2V0ICJudWV2byIsIGVuIHByaW1lciBsdWdhciwgdmlzdWFsaXphcmVtb3MgbGEgZGlzdHJpYnVjacOzbiBpbmljaWFsIGRlIGxvcyBkYXRvcyBhIHRyYXbDqXMgZGUgdW5hIGdyw6FmaWNhIDNELgoKYGBge3J9CnBsb3RfbHkoc21hbGxlcl9kZiwKICB4ID0gfmNhcC5kaWFtZXRlciwgeSA9IH5zdGVtLmhlaWdodCwKICB6ID0gfnN0ZW0ud2lkdGgKKSAlPiUKICBhZGRfbWFya2VycyhzaXplID0gMS41KQpgYGAKCkEgY29udGludWFjacOzbiwgdmFtb3MgYSBlc3R1ZGlhciBjdcOhbCBzZXLDrWEgZWwgbsO6bWVybyDDs3B0aW1vIGRlIGNsw7pzdGVyZXMgcGFyYSBlbCBkYXRhc2V0IHJlZHVjaWRvIGhhY2llbmRvIHVzbyBkZSBsYSBtZWRpZGEgZGUgYm9uZGFkIGludGVybmEgInNpbGhvdWV0dGUiLiBQYXJhIGVsbG8sIHV0aWxpemFyZW1vcyBsYSBmdW5jacOzbiBmdml6X25iY2x1c3QgZGUgZmFjdG9leHRyYS4KCkxhIG1lZGlkYSBzaWxob3VldHRlIHRvbWEgdmFsb3JlcyBlbnRyZSAtMSB5IDEuIFVuIHZhbG9yIGNlcmNhbm8gYSAxIGluZGljYSBxdWUgZWwgcHVudG8gZXN0w6EgYmllbiBhc2lnbmFkbyBhbCBjbMO6c3RlciB5IGxvcyBwdW50b3MgZGVsIGNsw7pzdGVyIHNvbiBtdXkgc2ltaWxhcmVzIGVudHJlIHPDrS4gVW4gdmFsb3IgY2VyY2FubyBhIDAgaW5kaWNhIHF1ZSBlbCBwdW50byBlc3TDoSBlbiB1biAiw6FyZWEgZ3JpcyIgeSBubyBlc3TDoSBjbGFyYW1lbnRlIGFzaWduYWRvIGEgbmluZ3VubyBkZSBsb3MgZG9zIGNsw7pzdGVyZXMuIFVuIHZhbG9yIGNlcmNhbm8gYSAtMSBpbmRpY2EgcXVlIGVsIHB1bnRvIGVzdMOhIG1hbCBhc2lnbmFkbyBhbCBjbMO6c3RlciB5IHNlcsOtYSBtw6FzIGFwcm9waWFkbyBwYXJhIG90cm8gY2zDunN0ZXIuCgpMYSBmdW5jacOzbiBmdml6X25iY2x1c3QoKSBkZSBsYSBsaWJyZXLDrWEgZmFjdG9leHRyYSBlbiBSIHBlcm1pdGUgdmlzdWFsaXphciBsYSBtZWRpZGEgc2lsaG91ZXR0ZSBwYXJhIGRpZmVyZW50ZXMgdmFsb3JlcyBkZSAiayIgKG7Dum1lcm8gZGUgY2zDunN0ZXJlcykgeSBheXVkYXIgYSBkZXRlcm1pbmFyIGVsIG7Dum1lcm8gw7NwdGltbyBkZSBjbMO6c3RlcmVzLiBBbCB1dGlsaXphciBlc3RhIGZ1bmNpw7NuLCBzZSBwdWVkZSBvYnRlbmVyIHVuIGdyw6FmaWNvIGVuIGVsIHF1ZSBzZSByZXByZXNlbnRhIGxhIG1lZGlkYSBzaWxob3VldHRlIGVuIGZ1bmNpw7NuIGRlbCBuw7ptZXJvIGRlIGNsw7pzdGVyZXMgeSBzZWxlY2Npb25hciBlbCB2YWxvciBkZSAiayIgZW4gZWwgcXVlIHNlIG9idGllbmUgZWwgbWF5b3IgdmFsb3IgZGUgc2lsaG91ZXR0ZS4KYGBge3J9CmZ2aXpfbmJjbHVzdChzbWFsbGVyX2RmLCBGVU5jbHVzdGVyID0ga21lYW5zLCBtZXRob2QgPSAic2lsaG91ZXR0ZSIpCmBgYAoKU2Vnw7puIGxhIGdyw6FmaWNhLCBwb2RlbW9zIGFmaXJtYXIgcXVlIGVsIG7Dum1lcm8gw7NwdGltbyBkZSBjbMO6c3RlcmVzIGVzIDIsIHlhIHF1ZSBlcyBlbCB2YWxvciBkZSAiayIgcXVlIG1heGltaXphIGxhIG1lZGlkYSBzaWxob3VldHRlLiBUYW1iacOpbiBwb2RlbW9zIG9ic2VydmFyIHF1ZSBlbCB2YWxvciBkZSBzaWxob3VldHRlIHBhcmEgImsiID0gMyBlcyBjZXJjYW5vIGFsIG9idGVuaWRvIHBhcmEgImsiID0gMi4gRXN0byBwdWVkZSBzZXIgZGViaWRvIGEgcXVlIG51ZXN0cm8gZGF0YXNldCwgcGVzZSBhIHNlciBiaW5hcmlvLCBjdWVudGEgY29uIGRhdG9zIG11eSBkaXNwZXJzb3MuCgpQb3Igw7psdGltbywgdmlzdWFsaXphbW9zIGxhIGRpc3RyaWJ1Y2nDs24gZGUgbG9zIGRhdG9zIGVuIGZ1bmNpw7NuIGRlIGxvcyBjbMO6c3RlcmVzIG9idGVuaWRvcy4KCmBgYHtyfQprbV9zbV9tb2RlbCA8LSBrbWVhbnMoc21hbGxlcl9kZiwgY2VudGVyID0gMiwgbnN0YXJ0ID0gMjApCmNsdXN0ZXIgPC0gZmFjdG9yKGttX3NtX21vZGVsJGNsdXN0ZXIpCgpwbG90X2x5KHNtYWxsZXJfZGYsCiAgeCA9IH5jYXAuZGlhbWV0ZXIsIHkgPSB+c3RlbS5oZWlnaHQsCiAgeiA9IH5zdGVtLndpZHRoLCBjb2xvciA9IH5jbHVzdGVyCikgJT4lCiAgYWRkX21hcmtlcnMoc2l6ZSA9IDEuNSkKYGBgCgpQb3Igw7psdGltbywgY2FsY3VsYW1vcyBlbCB2YWxvciBwcm9tZWRpbyBkZSBsYXMgdmFyaWFibGVzIHBhcmEgY2FkYSBjbMO6c3RlciBnZW5lcmFkbyBjb24gZWwgbW9kZWxvIGRlIGstbWVhbnMgcGFyYSBlbCBkYXRhc2V0IHJlZHVjaWRvLgpgYGB7cn0KZ3JvdXBlZF9zbV9tdXNocm9vbSA8LSBzbWFsbGVyX2RmICU+JQogIG11dGF0ZShjbHVzdGVyID0gY2x1c3RlcikgJT4lCiAgZ3JvdXBfYnkoY2x1c3RlcikgJT4lCiAgc3VtbWFyaXNlKAogICAgbWVhbl9jYXBfZGlhbWV0ZXIgPSBtZWFuKGNhcC5kaWFtZXRlciksCiAgICBtZWFuX3N0ZW1faGVpZ2h0ID0gbWVhbihzdGVtLmhlaWdodCksCiAgICBtZWFuX3N0ZW1fd2lkdGggPSBtZWFuKHN0ZW0ud2lkdGgpCiAgKQpncm91cGVkX3NtX211c2hyb29tCmBgYAoKIyMjIENsdXN0ZXJpbmcgamVyw6FycXVpY288YSBuYW1lPSJoaWVyYXJjaGljYWwiPjwvYT4KCkNsdXN0ZXJpbmcgamVyw6FycXVpY28gZXMgdW4gdGlwbyBkZSBhbGdvcml0bW8gZGUgY2x1c3RlcmluZyBxdWUgc2UgdXRpbGl6YSBwYXJhIGRpdmlkaXIgYSB1biBjb25qdW50byBkZSBkYXRvcyBlbiBncnVwb3MgKGNsw7pzdGVyZXMpIGRlIGZvcm1hIHF1ZSBsb3MgZGF0b3MgZW4gZWwgbWlzbW8gY2zDunN0ZXJzZWFuIHNpbWlsYXJlcyBlbnRyZSBzw60uIExhIGZ1bmNpw7NuIGhjbHVzdCBlcyB1bmEgZnVuY2nDs24gZW4gUiBxdWUgc2UgdXRpbGl6YSBwYXJhIHJlYWxpemFyIGNsdXN0ZXJpbmcgamVyw6FycXVpY28uCiAKQW50ZXMgZGUgYXBsaWNhciBlbCBhbGdvcml0bW8gaGNsdXN0LCBlcyBuZWNlc2FyaW8gY2FsY3VsYXIgbGFzIGRpc3RhbmNpYXMgZW50cmUgbG9zIHB1bnRvcyBkZWwgY29uanVudG8gZGUgZGF0b3MsIHBhcmEgZWxsbywgdXRpbGl6YXJlbW9zIGxhIGZ1bmNpw7NuIGRpc3QgZGUgUi4gTGEgZnVuY2nDs24gZGlzdCgpIGNhbGN1bGEgbGEgZGlzdGFuY2lhIGVudHJlIGxvcyBwdW50b3MgZGVsIGNvbmp1bnRvIGRlIGRhdG9zIHkgZGV2dWVsdmUgdW5hIG1hdHJpeiBkZSBkaXN0YW5jaWFzLiBQb3IgZGVmZWN0bywgbGEgZnVuY2nDs24gZGlzdCgpIHV0aWxpemEgbGEgZGlzdGFuY2lhIGV1Y2zDrWRlYSBwYXJhIGNhbGN1bGFyIGxhcyBkaXN0YW5jaWFzIGVudHJlIGxvcyBwdW50b3MgZGVsIGNvbmp1bnRvIGRlIGRhdG9zLgoKQ2FiZSBkZXN0YWNhciwgcXVlIGxhIGZ1bmNpw7NuIGhjbHVzdCBoYWNlIHVzbyBwb3IgZGVmZWN0byBkZWwgY8OhY3VsbyBkZSBkaXN0YW5jaWEgZW50cmUgY2zDunN0ZXJlcyBiYXNhZG8gZW4gZWwgbcOpdG9kbyBkZSAiQ29tcGxldGUiLiBFc3RlIG3DqXRvZG8gZGUgY8OhbGN1bG8gZGUgZGlzdGFuY2lhIGVudHJlIGNsw7pzdGVyZXMgc2UgYmFzYSBlbiBsYSBkaXN0YW5jaWEgZW50cmUgbG9zIHB1bnRvcyBtw6FzIGxlamFub3MgZGUgY2FkYSBjbHVzdGVyLgpgYGB7cn0KZGlzdGFuY2UgPC0gZGlzdChzbWFsbGVyX2RmKQpoY19tb2RlbCA8LSBoY2x1c3QoZGlzdGFuY2UpCmBgYAoKUmVwcmVzZW50YW1vcyBlbCBkZW5kcm9ncmFtYSBwYXJhIHZpc3VhbGl6YXIgbGEgZGlzdHJpYnVjacOzbiBkZSBsb3MgZGF0b3MgZW4gZnVuY2nDs24gZGUgbG9zIGNsw7pzdGVyZXMgb2J0ZW5pZG9zLgpgYGB7cn0KZGVuZF9tb2RlbG8gPC0gYXMuZGVuZHJvZ3JhbShoY19tb2RlbCkKcGxvdChkZW5kX21vZGVsbywgeWxhYiA9ICJTaW1pbGFyaXR5IikKYGBgCgpIYXN0YSBhaG9yYSwgaGVtb3Mgb2J0ZW5pZG8gbGEgamVyYXJxdcOtYSBkZSBsb3MgZGF0b3MsIHBlcm8gbG8gcXVlIHJlYWxtZW50ZSBub3MgaW50ZXJlc2EgZXMgbGEgY2xhc2lmaWNhY2nDs24gZGUgbG9zIGRhdG9zIGVuIGZ1bmNpw7NuIGRlIGxvcyBjbMO6c3RlcmVzLgpDb3J0YXJlbW9zIGVsIGRlbmRyb2dyYW1hIGVuIHVuIHB1bnRvIHF1ZSBub3MgaW50ZXJlc2UgcGFyYSBvYnRlbmVyIGxvcyBjbMO6c3RlcmVzLiBFbiBlc3RlIGNhc28sIHkgYSBtb2RvIGRlIHBydWViYSwgaGVtb3MgZGVjaWRpZG8gY29ydGFyIGVsIGRlbmRyb2dyYW1hIGVuIDkwIHBhcmEgb2J0ZW5lciB1bmEgdmlzdWFsaXphY2nDs24gZGVsIGRlbmRvZ3JhbWEgY29ydGFkby4KYGBge3J9CmN1dCA8LSAwLjkKCmRlbmRfbW9kZWxvICU+JQogIGNvbG9yX2JyYW5jaGVzKGggPSBjdXQpICU+JQogIGNvbG9yX2xhYmVscyhoID0gY3V0KSAlPiUKICBwbG90KHlsYWIgPSAiU2ltaWxhcml0eSIpCmBgYAoKUGFyYSBvYnRlbmVyIGVsIG7Dum1lcm8gw7NwdGltbyBkZSBjbMO6c3RlciwgaGFyZW1vcyB1c28gZGUgbGEgbWVkaWRhIGludGVybmEgZGUgYm9uZGFkIHNpbGhvdWV0dGUuIFBhcmEgZWxsbywgdXRpbGl6YXJlbW9zIGxhIGZ1bmNpw7NuIGZ2aXpfbmJjbHVzdCBkZSBmYWN0b2V4dHJhIGFsIGlndWFsIHF1ZSBjb24gay1tZWFucy4KCmBgYHtyfQpmdml6X25iY2x1c3Qoc21hbGxlcl9kZiwgRlVOY2x1c3RlciA9IGhjdXQsIG1ldGhvZCA9ICJzaWxob3VldHRlIikKYGBgCgpDb21wcm9iYW1vcyBxdWUgZW4gZXN0ZSBjYXNvLCBlbCBuw7ptZXJvIMOzcHRpbW8gZGUgY2zDunN0ZXJlcyBwb2Ryw61hIHNlciAyIG8gMywgeWEgcXVlIGVsIHZhbG9yIGRlIHNpbGhvdWV0dGUgZXMgbXV5IHNpbWlsYXIgcGFyYSBhbWJvcyBjYXNvcy4gRW4gZXN0ZSBjYXNvLCBoZW1vcyBkZWNpZGlkbyB1dGlsaXphciAyIGNsw7pzdGVyZXMgcGFyYSBwb2RlciBjb21wYXJhciBwb3N0ZXJpb3JtZW50ZSBsb3MgcmVzdWx0YWRvcyBjb24gbG9zIG9idGVuaWRvcyBjb24gZWwgYWxnb3JpdG1vIGRlIGstbWVhbnMuCgpQYXJhIGdlbmVyYXIgZWwgbW9kZWxvIGRlIGNsdXN0ZXJpbmcgamVyw6FycXVpY28sIHV0aWxpemFyZW1vcyBsYSBmdW5jacOzbiBjdXRyZWUgZGUgUi4gRXN0YSBmdW5jacOzbiBub3MgcGVybWl0ZSBnZW5lcmFyIGVsIG1vZGVsbyBkZSBjbHVzdGVyaW5nIGplcsOhcnF1aWNvIGVuIGZ1bmNpw7NuIGRlbCBuw7ptZXJvIGRlIGNsw7pzdGVyZXMgcXVlIHF1ZXJhbW9zIG9idGVuZXIuCgpDYWxjdWxhbW9zIGxhIGFncnVwYWNpw7NuIGRlbCBtb2RlbG8gZW4gZnVuY2nDs24gZGVsIG7Dum1lcm8gZGUgY2zDunN0ZXJlcyBxdWUgaGVtb3MgZGVjaWRpZG8gdXRpbGl6YXIsIHkgY2FsY3VsYW1vcyBlbCB2YWxvciBwcm9tZWRpbyBkZSBsYXMgdmFyaWFibGVzIHBhcmEgY2FkYSBjbMO6c3RlcmdlbmVyYWRvLgoKYGBge3J9CmpxX2NsdXN0ZXIgPC0gY3V0cmVlKGhjX21vZGVsLCBrID0gMikKCmdyb3VwZWRfbXVzaHJvb20gPC0gc21hbGxlcl9kZiAlPiUKICBtdXRhdGUoY2x1c3RlciA9IGpxX2NsdXN0ZXIpICU+JQogIGdyb3VwX2J5KGNsdXN0ZXIpICU+JQogIHN1bW1hcmlzZV9hbGwobWVhbikKZ3JvdXBlZF9tdXNocm9vbQpgYGAKClZpc3VhbGl6YW1vcyBsYSBhZ3J1cGFjacOzbiBkZSBsb3MgZGF0b3MgZW4gZnVuY2nDs24gZGUgbG9zIGNsw7pzdGVyZXMgb2J0ZW5pZG9zIGEgcGFydGlyIGRlbCBtb2RlbG8gZGUgY2x1c3RlcmluZyBqZXLDoXJxdWljby4KCmBgYHtyfQpqcV9jbHVzdGVyPC0gZmFjdG9yKGpxX2NsdXN0ZXIpCgpwbG90X2x5KHNtYWxsZXJfZGYsCiAgeCA9IH5jYXAuZGlhbWV0ZXIsIHkgPSB+c3RlbS5oZWlnaHQsCiAgeiA9IH5zdGVtLndpZHRoLAogIGNvbG9yID0gfmpxX2NsdXN0ZXIKKSAlPiUKICBhZGRfbWFya2VycyhzaXplID0gMS41KQpgYGAKCkNvbiBlbCBvYmpldGl2byBkZSBjb21wYXJhciBsb3MgcmVzdWx0YWRvcyBvYnRlbmlkb3MgZW4gbG9zIGRvcyBhbGdvcml0bW9zLCB2YW1vcyBhIGNhbGN1bGFyIGVsIHJlbmRpbWllbnRvIGRlIGNhZGEgdW5vIGRlIGVsbG9zLCBoYWNpZW5kbyB1c28gZGVsIGFjY3VyYWN5IGNvbW8gbWVkaWRhIGRlIGJvbmRhZCBleHRlcm5hLgoKRW4gcHJpbWVyIGx1Z2FyLCBjYWxjdWxhbW9zIGVsIGFjY3VyYWN5IGRlbCBtb2RlbG8gZGUgay1tZWFucy4gU3Vwb25kcmVtb3MgcXVlIGxhIGNsYXNlIDEgZXMgbGEgY2xhc2UgImUiIHkgbGEgY2xhc2UgMiBlcyBsYSBjbGFzZSAicCIuClBhcmEgZWxsbywgb2J0ZW5lbW9zIGxhcyBjbGFzZXMgcmVhbGVzIHkgbGFzIGNsYXNlcyBwcmVkaWNoYXMsIHkgY2FsY3VsYW1vcyBlbCBhY2N1cmFjeS4KClByaW1lcm8gbmVjZXNpdGFtb3Mgdm9sdmVyIGEgb2J0ZW5lciBlbCBkYXRhc2V0IHJlZHVjaWRvIHBhcmEgcG9kZXIgdGVuZXIgbGFzIGNsYXNlcyByZWFsZXMuCmBgYHtyfQpzbWFsbGVyX2RmIDwtIG11c2hyb29tW3NwbGl0JFJlc2FtcGxlMSwgXQpgYGAKCmBgYHtyfQpyZWFsX2NsYXNzZXMgPC0gaWZlbHNlKHNtYWxsZXJfZGYkY2xhc3MgPT0gImUiLCAxLCAyKQpwcmVkaWN0ZWRfY2xhc3NlcyA8LSBrbV9zbV9tb2RlbCRjbHVzdGVyCnByZWRpY3RlZF9jbGFzc2VzIDwtIGFzLm51bWVyaWMocHJlZGljdGVkX2NsYXNzZXMpCmBgYAoKYGBge3J9CmFjY3VyYWN5IDwtIHN1bShyZWFsX2NsYXNzZXMgPT0gcHJlZGljdGVkX2NsYXNzZXMpIC8gbGVuZ3RoKHJlYWxfY2xhc3NlcykKcHJpbnQoYWNjdXJhY3kpCmBgYAoKSGFjZW1vcyBsbyBtaXNtbyBjb24gZWwgbW9kZWxvIGRlIGNsdXN0ZXJpbmcgamVyw6FycXVpY28sIHBlcm8gZW4gZXN0ZSBjYXNvLCBzdXBvbmRyZW1vcyBxdWUgbGEgY2xhc2UgMSBlcyBsYSBjbGFzZSAicCIgeSBsYSBjbGFzZSAyIGVzIGxhIGNsYXNlICJlIi4KYGBge3J9CnJlYWxfY2xhc3NlcyA8LSBpZmVsc2Uoc21hbGxlcl9kZiRjbGFzcyA9PSAiZSIsIDIsIDEpCnByZWRpY3RlZF9jbGFzc2VzIDwtIGFzLm51bWVyaWMoanFfY2x1c3RlcikKYGBgCgpgYGB7cn0KYWNjdXJhY3kgPC0gc3VtKHJlYWxfY2xhc3NlcyA9PSBwcmVkaWN0ZWRfY2xhc3NlcykgLyBsZW5ndGgocmVhbF9jbGFzc2VzKQpwcmludChhY2N1cmFjeSkKYGBgCgpUcmFzIGNvbXBhcmFyIGxvcyByZXN1bHRhZG9zIG9idGVuaWRvcyBlbiBsb3MgZG9zIGFsZ29yaXRtb3MsIHBvZGVtb3MgYWZpcm1hciBxdWUgZWwgbW9kZWxvIGRlIGNsdXN0ZXJpbmcgamVyw6FycXVpY28gaGEgb2J0ZW5pZG8gdW4gYWNjdXJhY3kgbWF5b3IgcGFyYSBlc3RlIGRhdGFzZXQsIG9idGVuaWVuZG8gdW4gYWNjdXJhY3kgZGVsIDk4JSBmcmVudGUgYWwgNzAlIG9idGVuaWRvIHBvciBlbCBtb2RlbG8gZGUgay1tZWFucy4KClBlc2UgYSBvYnRlbmVyIHVuIGFjY3VyYWN5IG1heW9yIGNvbiBlbCBtb2RlbG8gZGUgY2x1c3RlcmluZyBqZXLDoXJxdWljbywgaGF5IHF1ZSB0ZW5lciBlbiBjdWVudGEgcXVlIGVsIG9iamV0aXZvIGRlIGFwcmVuZGl6YWplIG5vIHN1cGVydmlzYWRvIGVzIGxhIGFncnVwYWNpw7NuIGRlIGxvcyBkYXRvcyBlbiBmdW5jacOzbiBkZSBzdXMgY2FyYWN0ZXLDrXN0aWNhcywgeSBubyBsYSBwcmVkaWNjacOzbiBkZSB1bmEgdmFyaWFibGUgb2JqZXRpdm8uIEFkZW3DoXMsIGVsIGFjY3VyYWN5IG9idGVuaWRvIGNvbiBlbCBtb2RlbG8gZGUgY2x1c3RlcmluZyBqZXLDoXJxdWljbyBlcyBtdXkgYWx0bywgbG8gcXVlIHB1ZWRlIGRlYmVyc2UgYSBxdWUgZWwgZGF0YXNldCB1dGlsaXphZG8gZXMgbXV5IHJlZHVjaWRvIHkgbm8gcHJlc2VudGEgbXVjaGEgdmFyaWFiaWxpZGFkIGVudHJlIGxhcyBjbGFzZXMuCg==